﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Diagnostics;
    using System.DirectoryServices;
    using System.DirectoryServices.AccountManagement;
    using System.DirectoryServices.ActiveDirectory;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Security.AccessControl;
    using System.Security.Principal;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Threading.Tasks;
    #endregion

    public partial class ADDataAccess {

        #region Members
        public static LdapAdminLimits DefaultQueryPolicy { get; private set; }

        /// <summary>
        /// If true, errors accessing external forests are ignored.
        /// </summary>
        public static bool IgnoreExternalForestErrors { get; set; }

        public static bool Initialized { get; private set; }

        /// <summary>
        /// The threshold to limit recursive queries for group memberships.
        /// </summary>
        internal static int MaxGroupSearchRecursionDepth { get; set; }

        /// <summary>
        /// The maximum number of results to return in a single request.  Typically 1000.
        /// </summary>
        internal static int MaxPageSize { get; private set; }

        /// <summary>
        /// The maximum number of values that can be retrieved from a multivalued attribute in a single search request.
        /// </summary>
        internal static int MaxValRange { get; private set; }

        /// <summary>
        /// A convenient pre-defined AccessRule that can be used to check if an object has the "protect this object from deletion" box checked.
        /// </summary>
        public static ActiveDirectoryAccessRule ProtectedFromDeleteAccessRule { get; private set; }

        /// <summary>
        /// The amount of time to wait before failing a startup initialization.
        /// </summary>
        private static int StartupTimeoutSeconds { get; set; }

        private static object SyncRoot = new object();

        /// <summary>
        /// Specifies if Tombstoned objects are included in search results
        /// </summary>
        public static bool UseTombstonedObjects { get; set; }

        #region Collections
        /// <summary>
        /// Frequently accessed domain information fields for all domains
        /// </summary>
        public static IReadOnlyList<DomainConfiguration> ADDomainInfo { get; private set; }
        private static ConcurrentBag<DomainConfiguration> adDomainInfo { get; set; }


        /// <summary>
        /// Key: AD Domain Netbios Name Value: AD Domain object
        /// </summary>
        /// <remarks>Warning: The Domain object may throw an exception.</remarks>
        public static IReadOnlyDictionary<string, Domain> ADDomains { get; private set; }
        private static ConcurrentDictionary<string, Domain> adDomains { get; set; }

        /// <summary>
        /// Reads all of the attributes from the AD schema, and creates a complex type for each attribute.
        /// Key: attribute short name Value: SchemaAttributeProperties object
        /// </summary>
        /// <remarks>
        /// Approximately 1,300 in Windows Server 2008 R2, 3,500 attributes with Exchange attributes.
        /// </remarks>
        public static IReadOnlyDictionary<string, SchemaAttribute> ADSchemaAttributes { get; private set; }

        /// <summary>
        /// Key: Method name  Value: List of Exceptions that were thrown
        /// </summary>
        public static Dictionary<string, List<Exception>> Exceptions { get; private set; }

        /// <summary>
        /// Domains to exclude from operations and queries
        /// </summary>
        public static IReadOnlyList<string> ExcludedDomains { get; private set; }

        /// <summary>
        /// The DNS Name of external forests
        /// </summary>
        public static IReadOnlyList<string> ExternalForests { get; private set; }

        /// <summary>
        /// The order in which names that are queried may be resolved, similar to anr (ambiguous name resolution)
        /// </summary>
        public static IReadOnlyList<string> NameResolutionOrder { get; private set; }

        /// <summary>
        /// For queries where a list of attributes are specified, these attributes will be added if they are not present
        /// </summary>
        private static string[] RequiredQueryAttributes { get; set; }

        /// <summary>
        /// Issues with the built-in SIDs that exist only on domain controllers, and the Pre-Windows2000CompatibleAccess
        /// http://support.microsoft.com/kb/243330
        /// </summary>
        public static string[] SIDsToIgnore { get; private set; }

        /// <summary>
        /// SecurityIdentifiers that cannot be translated
        /// </summary>
        /// <remarks>Checking for an unknown SID here is less expensive than throwing an IdentityNotMapped exception.</remarks>
        internal static Dictionary<string, SecurityIdentifier> UnknownSIDs { get; set; }

        /// <summary>
        /// Well-known SecurityIdentifiers
        /// </summary>
        public static IReadOnlyDictionary<string, SecurityIdentifier> WellKnownSIDs { get; set; }
        #endregion

        #region Domain/Forest naming contexts distinguished names
        public static string DomainNamingMasterDN { get; private set; }
        public static string ForestRootConfigurationNamingContext { get; private set; }
        public static string ForestRootDomainNamingContext { get; private set; }
        public static string ForestRootDN { get; private set; }
        public static string ForestRootDNSName { get; private set; }
        public static string ForestRootSchemaNamingContext { get; private set; }
        public static string SchemaMasterDN { get; private set; }
        public static DateTime ForestSchemaModifyTimeStampUTC { get; private set; }
        #endregion

        #region Container classes for User/Group/... methods
        public static readonly ContactMethods ContactMethods;
        public static readonly ComputerAccountMethods ComputerAccountMethods;
        public static readonly GroupMethods GroupMethods;
        public static readonly OUMethods OUMethods;
        public static readonly QueryFilters QueryFilters;
        public static readonly SiteMethods SiteMethods;
        public static readonly UserAccountMethods UserAccountMethods;
        #endregion

        #region DistinguishedName flag strings
        public static readonly string ConflictFlag = @"\0ACNF:";
        public static readonly string TombstoneFlag = @"\0ADEL:";
        #endregion

        #region Name character filters
        #region MailNickname character filters
        internal static readonly string MailNicknameDoesNotContainInvalidCharactersFilter;
        internal static readonly char[] MailNicknameInvalidCharacters;
        public static readonly char[] MailNicknameValidCharacters;
        #endregion

        #region SamAccountName character filters
        /// <summary>
        /// Invalid characters for samAccountName attribute (Pre-Windows 2000 name) 
        /// </summary>
        /// Characters: \/[]:;|=,+*?<>"
        /// Regex characters that need to be escaped: []\^$.|?*+()
        internal static readonly string SamAccountNameDoesNotContainInvalidCharactersFilter;
        internal static readonly char[] SamAccountNameInvalidCharacters;
        #endregion

        #region DistinguishedName character filters
        /// <summary>
        /// Distinguished Names
        /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa366101%28v=vs.85%29.aspx 
        /// </summary>
        /// <![CDATA[
        /// Reserved characters:
        /// space or # character at the beginning of a string    0x20
        /// space character at the end of a string    0x20
        /// ,    comma    0x2C
        /// +    plus sign    0x2B
        /// "    double quote    0x22
        /// \    backslash    0x5C
        /// <    left angle bracket    0x3C
        /// >    right angle bracket    0x3E
        /// ;    semicolon    0x3B
        /// LF    line feed    0x0A
        /// CR    carriage return    0x0D
        /// =    equals sign    0x3D
        /// /    forwards slash    0x2F
        /// ,#+<>;"=/\ and space
        /// ]]>
        private static readonly string[] DNAllowedStrings;
        private static readonly string[] DNAlternateEscapedCharacters;
        private static readonly Dictionary<char, string> DNEscapedCharacterMap;
        #endregion
        #endregion
        #endregion

        #region Constructors
        static ADDataAccess() {
            Debug.WriteLine(string.Format("{0}  {1}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

            ADDomainInfo = new List<DomainConfiguration>();
            adDomainInfo = new ConcurrentBag<DomainConfiguration>();
            ADDomains = new Dictionary<string, Domain>(StringComparer.OrdinalIgnoreCase);
            adDomains = new ConcurrentDictionary<string, Domain>(StringComparer.OrdinalIgnoreCase);
            ADSchemaAttributes = new Dictionary<string, SchemaAttribute>(StringComparer.OrdinalIgnoreCase);
            NameResolutionOrder = new List<string>();

            SIDsToIgnore = new string[] {
                new SecurityIdentifier(WellKnownSidType.BuiltinAccountOperatorsSid, null).Value,
                new SecurityIdentifier(WellKnownSidType.BuiltinPrintOperatorsSid, null).Value,
                new SecurityIdentifier(WellKnownSidType.BuiltinSystemOperatorsSid, null).Value,
                new SecurityIdentifier(WellKnownSidType.BuiltinPreWindows2000CompatibleAccessSid, null).Value
            };

            IgnoreExternalForestErrors = ConfigurationManager.AppSettings["IgnoreExternalForestErrors"] != null ? Convert.ToBoolean(ConfigurationManager.AppSettings["IgnoreExternalForestErrors"]) : false;
            StartupTimeoutSeconds = ConfigurationManager.AppSettings["StartupTimeoutSeconds"] != null ? Convert.ToInt32(ConfigurationManager.AppSettings["StartupTimeoutSeconds"]) : 90;
            UseTombstonedObjects = ConfigurationManager.AppSettings["UseTombstonedObjects"] != null ? Convert.ToBoolean(ConfigurationManager.AppSettings["UseTombstonedObjects"]) : false;
            ExcludedDomains = ConfigurationManager.AppSettings["ExcludedDomains"] != null ? new List<string>(ConfigurationManager.AppSettings["ExcludedDomains"].Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) : new List<string>();
            ExternalForests = ConfigurationManager.AppSettings["ExternalForests"] != null ? new List<string>(ConfigurationManager.AppSettings["ExternalForests"].Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) : new List<string>();
            NameResolutionOrder = ConfigurationManager.AppSettings["NameResolutionOrder"] != null ? new List<string>(ConfigurationManager.AppSettings["NameResolutionOrder"].Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) : new List<string>();

            MaxGroupSearchRecursionDepth = 100;

            Exceptions = new Dictionary<string, List<Exception>>(StringComparer.OrdinalIgnoreCase);

            ProtectedFromDeleteAccessRule = new ActiveDirectoryAccessRule(
                new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                (ActiveDirectoryRights.Delete | ActiveDirectoryRights.DeleteChild | ActiveDirectoryRights.DeleteTree),
                AccessControlType.Deny,
                ActiveDirectorySecurityInheritance.None);

            MailNicknameDoesNotContainInvalidCharactersFilter = @"^[^\\/\[\]:;\|=,\+\*\?\""]*$";
            MailNicknameInvalidCharacters = new char[] { '\\', '/', '[', ']', ':', ';', '|', '=', ',', '+', '*', '?', '<', '>', '"' };
            //  A to Z (uppercase or lowercase), digits from 0 to 9, !, #, $, %, &, ‘, *, +, -, /, =, ?, ^, _, `, {, |, } or ~. 
            MailNicknameValidCharacters = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '#', '$', '%', '&', '‘', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~', '.' };

            // Characters: \/[]:;|=,+*?<>"
            SamAccountNameDoesNotContainInvalidCharactersFilter = @"^[^\\/\[\]:;\|=,\+\*\?<>\""]*$";
            SamAccountNameInvalidCharacters = new char[] { '\\', '/', '[', ']', ':', ';', '|', '=', ',', '+', '*', '?', '<', '>', '"' };

            DNAllowedStrings = new string[] { ", CN=", ",CN=", "CN=", "\\ ,CN=", ", OU=", ",OU=", "OU=", "\\ ,OU=", ", DC=", ",DC=", "DC=", "\\ ,DC=", ADDataAccess.ConflictFlag, ADDataAccess.TombstoneFlag };
            DNAlternateEscapedCharacters = new string[] { "\\,", "\\#", "\\+", "\\<", "\\>", "\\;", "\\\"", "\\=", "\\/", "\\\\", "\\\\\\", "\\ ", "CN=\\ ", "OU=\\ ", "DC=\\ ", "\\ ,CN=", "\\ ,OU=", "\\ ,DC=" };
            DNEscapedCharacterMap = new Dictionary<char, string>();

            ADDataAccess.GroupMethods = new GroupMethods();
            ADDataAccess.ComputerAccountMethods = new ComputerAccountMethods();
            ADDataAccess.ContactMethods = new ContactMethods();
            ADDataAccess.OUMethods = new OUMethods();
            ADDataAccess.QueryFilters = new QueryFilters();
            ADDataAccess.SiteMethods = new SiteMethods();
            ADDataAccess.UserAccountMethods = new UserAccountMethods();

            /// ,    comma    0x2C
            /// +    plus sign    0x2B
            /// "    double quote    0x22
            /// \    backslash    0x5C
            /// <    left angle bracket    0x3C
            /// >    right angle bracket    0x3E
            /// ;    semicolon    0x3B
            /// LF    line feed    0x0A
            /// CR    carriage return    0x0D
            /// =    equals sign    0x3D
            /// /    forwards slash    0x2F
            DNEscapedCharacterMap.Add(' ', @"\20");
            DNEscapedCharacterMap.Add('#', @"\23");
            DNEscapedCharacterMap.Add(',', @"\2C");
            DNEscapedCharacterMap.Add('+', @"\2B");
            DNEscapedCharacterMap.Add('"', @"\22");
            DNEscapedCharacterMap.Add('\\', @"\5C");
            DNEscapedCharacterMap.Add('<', @"\3C");
            DNEscapedCharacterMap.Add('>', @"\3E");
            DNEscapedCharacterMap.Add(';', @"\3B");
            DNEscapedCharacterMap.Add('=', @"\3D");
            DNEscapedCharacterMap.Add('/', @"\2F");

            RequiredQueryAttributes = new string[] { "objectCategory", "objectClass", "objectGuid", "systemFlags" };

        }
        private ADDataAccess() {
            Debug.WriteLine(string.Format("{0}  {1}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));
            //if (!Initialized) Initialize();
        }
        #endregion

        #region Methods

        #region Initialization methods
        /// <summary>
        /// Builds the collections for Domains, WellKnownSIDs
        /// </summary>
        /// <returns>True if successful</returns>
        private static bool BuildDictionaries() {
            Debug.WriteLine(string.Format("{0}  {1}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

            var stopwatch = Stopwatch.StartNew();
            try {
                var domains = new List<Domain>();

                Forest currentForest = Forest.GetCurrentForest();
                ForestRootDNSName = currentForest.Name;

                foreach (Domain domain in currentForest.Domains) {
                    domains.Add(domain);
                }

                #region Get domains in external forests
                foreach (var forestDNSName in ExternalForests) {
                    Debug.WriteLine(string.Format("{0}  {1} Getting domain information for external forest: {2}",
                        DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), forestDNSName));
                    try {
                        var directoryContext = new DirectoryContext(DirectoryContextType.Forest, forestDNSName);
                        var directoryContextDomains = Forest.GetForest(directoryContext).Domains;
                        foreach (Domain domain in directoryContextDomains) {
                            if (domain == null) {
                                Debug.WriteLine(string.Format("{0}  {1} WARNING: Null domain (UNKNOWN) returned from Forest.GetForest(directoryContext).Domains. forestDNSName: {2}",
                                    DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), forestDNSName));
                                continue;
                            }
                            domains.Add(domain);
                        }
                    }
                    catch (Exception e) {
                        Debug.WriteLine(string.Format("{0}  {1} ERROR Getting domain information for external forest: {2} {3}",
                            DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                            forestDNSName, e.VerboseExceptionString()));
                        if (!IgnoreExternalForestErrors) throw;
                    }
                }
                #endregion

                using (var cts = new CancellationTokenSource()) {
                    var tasks = new List<Task>();
                    foreach (var domain in domains) {
                        tasks.Add(Task.Run(() => {
                            BuildDomainList(domain);
                        }, cts.Token));
                    }

                    var timeout = TimeSpan.FromSeconds(StartupTimeoutSeconds);
                    if (!Task.WaitAll(tasks.ToArray(), timeout)) {
                        cts.Cancel();
                        throw new ApplicationException("Timeout waiting for threadpool threads to complete in BuildDomainList.");
                    }
                }

                ADDomainInfo = adDomainInfo.ToList();
                ADDomains = adDomains.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase);

                var domainStats = new Dictionary<string, List<DomainConfiguration>>(StringComparer.OrdinalIgnoreCase);
                foreach (var domainInfo in ADDomainInfo) {
                    if (!domainStats.ContainsKey(domainInfo.ForestName)) {
                        domainStats.Add(domainInfo.ForestName, new List<DomainConfiguration>());
                    }
                    domainStats[domainInfo.ForestName].Add(domainInfo);
                }

                foreach (var stat in domainStats) {
                    Debug.WriteLine(string.Format("{0}  {1} Forest: {2} Domain count: {3}",
                        DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), stat.Key, stat.Value.Count));

                    foreach (var domainInfo in stat.Value) {
                        Debug.WriteLine(string.Format("{0}  {1}  - Forest: {2} Domain DN: {3} Domain DNS Name: {4} Domain NetBios Name: {5}",
                            DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                            domainInfo.ForestName, domainInfo.DistinguishedName, domainInfo.DNSName, domainInfo.NetbiosName));
                    }
                }

                UnknownSIDs = new Dictionary<string, SecurityIdentifier>(StringComparer.OrdinalIgnoreCase);

                // get list of domain sids
                using (var forestRootDomainDirectoryEntry = currentForest.RootDomain.GetDirectoryEntry()) {

                    WellKnownSIDs = new Dictionary<string, SecurityIdentifier>(StringComparer.OrdinalIgnoreCase);
                    byte[] forestRootObjectSid = (byte[])forestRootDomainDirectoryEntry.Properties["objectSid"][0];
                    var domainSid = new SecurityIdentifier(forestRootObjectSid, 0);

                    var wellKnownSIDs = new Dictionary<string, SecurityIdentifier>();
                    foreach (WellKnownSidType sidType in Enum.GetValues(typeof(WellKnownSidType))) {
                        try {
                            if (sidType == WellKnownSidType.LogonIdsSid) continue;
                            var sid = new SecurityIdentifier(sidType, domainSid);
                            if (!wellKnownSIDs.ContainsKey(sid.Value)) {
                                wellKnownSIDs.Add(sid.Value, sid);
                            }
                        }
                        catch { }
                    }

                    WellKnownSIDs = wellKnownSIDs;
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  {1} {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                return false;
            }
            finally {
                Debug.WriteLine(string.Format("{0}  {1} Finished.  Time required: {2}  ADDomains dictionary count: {3} ADDomainInfo collection count: {4}",
                    DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                    stopwatch.Elapsed, ADDomains.Count, ADDomainInfo.Count));
            }

            return (ADDomainInfo.Count > 0) && (WellKnownSIDs.Count > 0);
        }

        /// <summary>
        /// Builds the ADDomains collection for the specified domain
        /// </summary>
        /// <param name="domain"></param>
        private static void BuildDomainList(Domain domain) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Domain: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), domain.Name));
            var domainNetBiosName = string.Empty;
            var domainDN = string.Empty;

            try {
                if (ExcludedDomains.Any(x => domain.Name.StartsWith(x, StringComparison.OrdinalIgnoreCase))) return;

                domainNetBiosName = GetDomainNetBiosName(domain.Forest.RootDomain.Name, domain.Name);
                if (ExcludedDomains.Any(x => string.Equals(x, domainNetBiosName, StringComparison.OrdinalIgnoreCase))) return;

                using (var directoryEntry = domain.GetDirectoryEntry()) {
                    domainDN = GetDNFromLdapDN(directoryEntry.Path);
                    byte[] objectSidByteArray = directoryEntry.Properties["objectSid"][0] as byte[];
                    byte[] objectGuidByteArray = directoryEntry.Properties["objectGuid"][0] as byte[];

                    if ((objectSidByteArray != null) && (objectGuidByteArray != null)) {
                        var sid = new SecurityIdentifier(objectSidByteArray, 0);
                        var domainGuid = new Guid(objectGuidByteArray);

                        var isRootDomain = string.Equals(domain.Name, domain.Forest.RootDomain.Name, StringComparison.OrdinalIgnoreCase);

                        var domainConfiguration = new DomainConfiguration(
                            domainDN, domain.Name, domain.Forest.Name, domainNetBiosName, sid, domainGuid, isRootDomain);

                        adDomainInfo.Add(domainConfiguration);
                        adDomains.AddOrUpdate(domainNetBiosName, domain, (TKey, TOldValue) => domain);
                    }
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  {1} Domain: {2} Exception: {3}",
                    DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), domain.Name, e.VerboseExceptionString()));
                throw;
            }
            finally {
                Debug.WriteLine(string.Format("{0}  {1} Finished getting information for domain: {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), domain.Name));
            }
        }

        private static void GetSchemaAttributes() {
            Debug.WriteLine(string.Format("{0}  {1} Building dictionary of AD schema attributes.", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

            var stopwatch = Stopwatch.StartNew();
            var adSchemaAttributes = new Dictionary<string, SchemaAttribute>(StringComparer.OrdinalIgnoreCase);
            string filter = string.Format("(objectCategory=attributeSchema)");
            var adAttributes = GetADObjectProperties<SchemaAttribute>(
                ForestRootSchemaNamingContext, SchemaAttribute.AttributesToGet, filter, PartitionScope.LocalPartition);

            if (adAttributes != null) {
                foreach (var adAttribute in adAttributes) {
                    if (adAttribute.IsValid) {
                        if (!adSchemaAttributes.ContainsKey(adAttribute.LDAPDisplayName)) {
                            adSchemaAttributes.Add(adAttribute.LDAPDisplayName, adAttribute);
                        }
                    }
                }

                ADSchemaAttributes = adSchemaAttributes
                    .OrderBy(x => x.Key)
                    .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
            }

            Debug.WriteLine(string.Format("{0}  {1} AD schema attributes found: {2}.  Time required: {3}",
                DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), ADSchemaAttributes.Count, stopwatch.Elapsed));
        }

        private static void GetSchemaModifyTimeStamp() {
            Debug.WriteLine(string.Format("{0}  {1}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));
            var stopwatch = Stopwatch.StartNew();

            string filter = string.Format("(&(objectCategory=subSchema)(Name=Aggregate))");
            var adAttributes = GetADObjectProperties<ADBaseObjectProperties>(
                ForestRootSchemaNamingContext, ADBaseObjectProperties.AttributesToGet, filter, PartitionScope.LocalPartition);

            if (adAttributes != null) {
                foreach (var attribute in adAttributes) {
                    if (string.Equals(attribute.Name, "Aggregate", StringComparison.OrdinalIgnoreCase)) {
                        ForestSchemaModifyTimeStampUTC = attribute.ModifyTimeStampUTC;
                        break;
                    }
                }
            }

            Debug.WriteLine(string.Format("{0}  {1} ForestSchemaModifyTimeStampUTC: {2}. Time required: {3}",
                DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                ForestSchemaModifyTimeStampUTC.YMDHMSFriendly(), stopwatch.Elapsed));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <remarks>Do not call from static constructor.</remarks>
        public static bool Initialize() {
            Debug.WriteLine(string.Format("{0}  {1}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

            var stopwatch = Stopwatch.StartNew();

            lock (SyncRoot) {
                if (Initialized) return true;
                Debug.WriteLine(string.Format("{0}  Getting AD Forest Root Directory Entries", DateTime.Now.TimeOfDay));

                var rootDSEBindPath = "LDAP://RootDSE";
                var authenticationType = GetAdjustedAuthenticationType(rootDSEBindPath);

                using (var forestRootDSEDirectoryEntry = new DirectoryEntry(rootDSEBindPath, null, null, authenticationType)) {
                    ForestRootDomainNamingContext = forestRootDSEDirectoryEntry.Properties["rootDomainNamingContext"][0].ToString();
                    ForestRootConfigurationNamingContext = forestRootDSEDirectoryEntry.Properties["configurationNamingContext"][0].ToString();
                    ForestRootSchemaNamingContext = forestRootDSEDirectoryEntry.Properties["schemaNamingContext"][0].ToString();

                    DomainNamingMasterDN = string.Format("CN=Partitions,{0}", ForestRootConfigurationNamingContext);
                    SchemaMasterDN = string.Format("CN=Schema,{0}", ForestRootConfigurationNamingContext);
                }

                ForestRootDN = GetDNFromLdapDN(ForestRootDomainNamingContext);

                adDomains = new ConcurrentDictionary<string, Domain>(StringComparer.CurrentCultureIgnoreCase);
                adDomainInfo = new ConcurrentBag<DomainConfiguration>();

                if (BuildDictionaries()) {
                    if ((ADDomainInfo.Count > 0) && (ADDomains.Count > 0)) {
                        if (ConnectionManager.Initialize()) {
                            Initialized = true;

                            DefaultQueryPolicy = new LdapAdminLimits();
                            MaxPageSize = ConfigurationManager.AppSettings["MaxPageSize"] != null
                                ? Convert.ToInt32(ConfigurationManager.AppSettings["MaxPageSize"])
                                : DefaultQueryPolicy.MaxPageSize;
                            MaxValRange = ConfigurationManager.AppSettings["MaxValRange"] != null
                                ? Convert.ToInt32(ConfigurationManager.AppSettings["MaxValRange"])
                                : DefaultQueryPolicy.MaxValRange;

                            GetSchemaAttributes();
                            GetSchemaModifyTimeStamp();
                        }
                    }
                }
            }

            Debug.WriteLine(string.Format("{0}  {1} Finished.  Time required: {2}.  Initialized successfully: {3}",
                DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                stopwatch.Elapsed, Initialized));

            if (!Initialized) {
                try {
                    Debug.WriteLine(string.Format("{0}  {1} ADDomains: ", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

                    foreach (var item in ADDomains) {
                        Debug.WriteLine(string.Format("{0}  {1}  - Domain: {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), item.Key));
                    }

                    Debug.WriteLine(string.Format("{0}  {1} ADDomainInfos: ", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName()));

                    foreach (var item in ADDomainInfo) {
                        Debug.WriteLine(string.Format("{0}  {1}  - DomainInfo: {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), item.DistinguishedName));
                    }
                }
                catch { }
            }

            return Initialized;
        }
        #endregion

        #region Helper methods
        #region DN parsing
        /// <summary>
        /// Determines if the bind path contains a server name.  If so, specify AuthenticationTypes.ServerBind
        /// </summary>
        /// <param name="bindPath">The LDAP bind path</param>
        /// <param name="authenticationType">The specified AuthenticationType (optional)</param>
        /// <returns>The AuthenticationTypes</returns>
        /// <remarks>
        /// LDAP connects take longer when targeting fully qualified DNS hostnames 
        /// https://support.microsoft.com/en-us/kb/2439492
        /// 
        /// When using server binds, speciy AuthenticationTypes.ServerBind. This is due to Windows may use broadcasts to perform server lookups.
        /// Windows 7 / Windows Server 2008 R2 introduced a name lookup behavior change to fix two broken scenarios:
        ///  - LDAP clients would fall back to NTLM whenever the NetBIOS domain name was supplied as the hostname in the LDAP connection.
        ///  AND
        ///  - LDAP clients would fail to connect to a DC in the domain if a client had the same name as the targeted NetBIOS domain name.
        ///  The delay is introduced by the wait time for a broadcast response. You will not see this delay if NetBt name resolution through
        ///  broadcasts is turned off.
        /// </remarks>
        [DebuggerStepThroughAttribute]
        public static AuthenticationTypes GetAdjustedAuthenticationType(string bindPath, AuthenticationTypes authenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.Signing) {
            if (string.IsNullOrWhiteSpace(bindPath)) return authenticationType;

            int doubleBackslash = bindPath.IndexOf("//");
            if (doubleBackslash > -1) {
                // example of bind path with server: LDAP://contosomddc1.contoso.com/DC=contoso,DC=com
                var subPath = bindPath.Substring(doubleBackslash + "//".Length);

                bool singleBackslash = (subPath.IndexOf("/CN=", StringComparison.OrdinalIgnoreCase) > -1)
                    || (subPath.IndexOf("/DC=", StringComparison.OrdinalIgnoreCase) > -1)
                    || (subPath.IndexOf("/OU=", StringComparison.OrdinalIgnoreCase) > -1);
                if (singleBackslash) {
                    if (!authenticationType.HasFlag(AuthenticationTypes.ServerBind)) {
                        authenticationType |= AuthenticationTypes.ServerBind;
                    }
                }
                else {
                    if (authenticationType.HasFlag(AuthenticationTypes.ServerBind)) {
                        authenticationType &= ~AuthenticationTypes.ServerBind;
                    }
                }
            }

            return authenticationType;
        }

        /// <summary>
        /// Inserts Preferred domain controller, if applicable
        /// Encodes special characters
        /// </summary>
        /// <param name="bindPath"></param>
        /// <param name="useGC"></param>
        /// <returns></returns>
        [DebuggerStepThroughAttribute]
        public static string GetAdjustedBindPath(bool forupdate, string bindPath) {
            return GetAdjustedBindPath(bindPath, PartitionScope.LocalPartition, forupdate);
        }

        [DebuggerStepThroughAttribute]
        public static string GetAdjustedBindPath(string bindPath, PartitionScope partitionScope) {
            return GetAdjustedBindPath(bindPath, partitionScope, forUpdate: false);
        }

        [DebuggerStepThroughAttribute]
        public static string GetAdjustedBindPath(string bindPath, PartitionScope partitionScope, bool forUpdate) {
            if (!Initialized) throw new ApplicationException("ADDataAccess not initialized.");
            if (partitionScope == PartitionScope.GlobalCatalog && forUpdate) {
                throw new ApplicationException("Both 'useGC' and 'forUpdate' are true.  Global Catalogs cannot be used for updates.");
            }
            string bindPathPrefix = partitionScope == PartitionScope.GlobalCatalog ? "GC://" : "LDAP://";

            if (!string.IsNullOrWhiteSpace(bindPath)) bindPath = bindPath.Trim();

            string domainNetBiosName = GetDomainNetBiosNameFromDNOrDNSName(bindPath);

            if (string.IsNullOrWhiteSpace(domainNetBiosName)) {
                throw new ApplicationException(string.Format("Netbios name not found for bind path: {0}", bindPath ?? "NULL"));
            }

            var availableDC = ConnectionManager.SelectDC(domainNetBiosName, partitionScope, forUpdate);

            if (availableDC == null) {
                throw new ApplicationException(string.Format("Domain controller not found for bind path: {0}", bindPath ?? "NULL"));
            }

            string adjustedBindPath = bindPathPrefix + availableDC.DNSName + "/" + DNEncode(GetDNFromLdapDN(bindPath));
            return adjustedBindPath;
        }

        public static string GetCNFromDN(string dn) {
            if (string.IsNullOrWhiteSpace(dn)) return string.Empty;
            if (!dn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase)) return string.Empty;

            string cn = dn.Substring("CN=".Length);

            if (dn.IndexOf(",OU=", StringComparison.OrdinalIgnoreCase) > 0) {
                cn = cn.Substring(0, cn.IndexOf(",OU=", StringComparison.OrdinalIgnoreCase));
            }
            else if (dn.IndexOf(",CN=", StringComparison.OrdinalIgnoreCase) > 0) {
                cn = cn.Substring(0, cn.IndexOf(",CN=", StringComparison.OrdinalIgnoreCase));
            }
            else {
                cn = cn.Substring(0, cn.IndexOf(",DC=", StringComparison.OrdinalIgnoreCase));
            }

            return cn;
        }

        /// <summary>
        /// Removes any LDAP or GC prefix
        /// Removes preferred domain controller
        /// </summary>
        /// <param name="ldapDN"></param>
        /// <returns>The constructed distinguished name</returns>
        /// <remarks>The constructed distinguished name may contain escape '\' characters</remarks>
        [DebuggerStepThroughAttribute]
        public static string GetDNFromLdapDN(string ldapDN) {
            string dn = string.Empty;
            if (!string.IsNullOrWhiteSpace(ldapDN)) ldapDN = ldapDN.Trim();

            if (ldapDN.StartsWith("LDAP://", StringComparison.OrdinalIgnoreCase)) {
                dn = ldapDN.Substring("LDAP://".Length);
            }
            else if (ldapDN.StartsWith("GC://", StringComparison.OrdinalIgnoreCase)) {
                dn = ldapDN.Substring("GC://".Length);
            }
            else {
                dn = ldapDN;
            }

            // if the ldapDN contains a specified server, remove that 
            if (dn.IndexOf("/") > 0) {
                // need to ensure that the slash is not part of the DN
                // check location of the first "=", such as CN= OU= DC=

                if (dn.IndexOf("=") > dn.IndexOf("/")) {
                    dn = dn.Substring(dn.IndexOf("/") + 1);
                }
            }

            return dn;
        }

        [DebuggerStepThroughAttribute]
        private static string GetDomainNetBiosName(string forestDnsName, string domainDnsName) {
            Debug.WriteLine(string.Format("{0}  {1} for forest: {2} domain: {3}",
                DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), forestDnsName ?? "NULL", domainDnsName ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(forestDnsName)) {
                throw new ArgumentNullException("forestDnsName");
            }
            if (string.IsNullOrWhiteSpace(domainDnsName)) {
                throw new ArgumentNullException("domainDnsName");
            }
            #endregion

            string domainNetBiosName = string.Empty;
            int attemptCount = 0;

            while (attemptCount < 5) {

                var rootDSEPath = string.Empty;
                var forestConfigDEPath = string.Empty;

                try {

                    rootDSEPath = string.Format("LDAP://{0}/RootDSE", forestDnsName);
                    var rootDSEAuthenticationType = GetAdjustedAuthenticationType(rootDSEPath);

                    using (var forestRootDSEDirectoryEntry = new DirectoryEntry(rootDSEPath, null, null, rootDSEAuthenticationType)) {
                        var forestRootConfigurationNamingContext = forestRootDSEDirectoryEntry.Properties["configurationNamingContext"][0].ToString();
                        forestConfigDEPath = string.Format("LDAP://{0}/CN=Partitions,{1}", forestDnsName, forestRootConfigurationNamingContext);
                        var faorestConfigDEAuthenticationType = GetAdjustedAuthenticationType(forestConfigDEPath);

                        using (var forestRootConfigurationPartitionsDirectoryEntry = new DirectoryEntry(forestConfigDEPath, null, null, faorestConfigDEAuthenticationType))
                        using (var directorySearcher = new DirectorySearcher(forestRootConfigurationPartitionsDirectoryEntry)) {
                            directorySearcher.Filter = string.Format("(&(nETBIOSName=*)(dnsRoot={0}))", domainDnsName);
                            directorySearcher.PropertiesToLoad.AddRange(new String[] { "dnsRoot", "nETBIOSName" });
                            directorySearcher.Tombstone = false;
                            var result = directorySearcher.FindOne();

                            if ((result != null) && (result.Properties.Contains("nETBIOSName"))) {
                                domainNetBiosName = result.Properties["nETBIOSName"][0].ToString();
                                break;
                            }
                            else {
                                Thread.Sleep(TimeSpan.FromSeconds(1));
                            }
                        }
                    }
                }
                catch (Exception e) {
                    Debug.WriteLine(string.Format("{0}  {1} error getting Netbios name for forest: {2} domain: {3} rootDsePath: {4}  forestConfigDEPath: {5} {6}",
                        DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(),
                        forestDnsName, domainDnsName, rootDSEPath, forestConfigDEPath, e.VerboseExceptionString()));
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            }

            return domainNetBiosName;
        }

        [DebuggerStepThroughAttribute]
        public static string GetDomainNetBiosNameFromDNOrDNSName(string dnOrDnsName) {
            if (string.IsNullOrWhiteSpace(dnOrDnsName)) return null;
            string domainNetBiosName = string.Empty;
            dnOrDnsName = dnOrDnsName.Trim();
            string domainDN = string.Empty;

            #region Check for DN
            if (dnOrDnsName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) > -1) {
                domainDN = GetDomainDNFromDN(dnOrDnsName);

                if (!string.IsNullOrWhiteSpace(domainDN)) {
                    if (ADDomainInfo.Count > 0) {
                        if (!ExcludedDomains.Any(x => (dnOrDnsName.StartsWith("DC=" + x, StringComparison.OrdinalIgnoreCase)))) {
                            DomainConfiguration domainInfo = ADDomainInfo.Where(x => string.Equals(x.DistinguishedName, domainDN, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

                            if ((domainInfo != null) && !ExcludedDomains.Any(x => string.Equals(domainInfo.NetbiosName, x, StringComparison.OrdinalIgnoreCase))) {
                                domainNetBiosName = domainInfo.NetbiosName;
                            }
                            else {
                                Debug.WriteLine(string.Format("{0}  {1} ADDomainInfo does not have an DN entry for dn: {2} dnOrDnsName: {3}",
                                    DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), domainDN, dnOrDnsName));
                            }
                        }
                    }
                }
            }
            #endregion
            #region Check for DNS name
            if (string.IsNullOrWhiteSpace(domainNetBiosName) && dnOrDnsName.Contains(".")) {
                if (ADDomainInfo.Count > 0) {
                    // first try for the fqdn of the domain
                    DomainConfiguration domainInfo = ADDomainInfo
                        .Where(x => string.Equals(x.DNSName, dnOrDnsName, StringComparison.OrdinalIgnoreCase))
                        .FirstOrDefault();
                    if (domainInfo != null) {
                        domainNetBiosName = domainInfo.NetbiosName;
                    }
                    else {
                        // remove the first hostname from the dns name, and check that value
                        if (dnOrDnsName.IndexOf(".") < dnOrDnsName.Length - 2) {
                            string dnsName = dnOrDnsName.Substring(dnOrDnsName.IndexOf(".") + 1);
                            domainInfo = ADDomainInfo
                                .Where(x => string.Equals(x.DNSName, dnsName, StringComparison.OrdinalIgnoreCase))
                                .FirstOrDefault();
                            if (domainInfo != null) {
                                domainNetBiosName = domainInfo.NetbiosName;
                            }
                        }
                    }
                }
            }
            #endregion

            #region Check if the supplied value is actually the domain netbios name
            if (string.IsNullOrWhiteSpace(domainNetBiosName)) {
                if (ADDomainInfo.Count > 0) {
                    DomainConfiguration domainInfo = ADDomainInfo
                        .Where(x => string.Equals(x.NetbiosName, dnOrDnsName, StringComparison.OrdinalIgnoreCase))
                        .FirstOrDefault();
                    if (domainInfo != null) {
                        domainNetBiosName = domainInfo.NetbiosName;
                    }
                }
            }
            #endregion

            if (string.IsNullOrWhiteSpace(domainNetBiosName)) {
                Debug.WriteLine(string.Format("{0}  {1} Warning: Domain NetBios name not found for dnOrDnsName: {2}",
                    DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), dnOrDnsName));
            }
            return domainNetBiosName;
        }

        [DebuggerStepThroughAttribute]
        public static string GetDomainDnsNameFromCanonicalName(string canonicalName) {
            if (string.IsNullOrWhiteSpace(canonicalName)) return null;
            string domainDnsName = null;
            canonicalName = canonicalName.Trim();

            if (canonicalName.IndexOf("/") > 0) {
                domainDnsName = canonicalName.Substring(0, canonicalName.IndexOf("/"));
            }
            return domainDnsName;
        }

        [DebuggerStepThroughAttribute]
        public static string GetDomainDnsName(string dn) {
            string domainDnsName = null;
            string domainDN = GetDomainDNFromDN(dn);

            if (domainDN != null) {
                domainDnsName = domainDN.Replace(",DC=", ".").Replace("DC=", string.Empty);
            }
            return domainDnsName;
        }

        [DebuggerStepThroughAttribute]
        public static string GetDomainDNFromDN(string dn) {

            string domainDN = string.Empty;
            if (!string.IsNullOrWhiteSpace(dn)) dn = dn.Trim();

            string microsoftDnsTag = "CN=MicrosoftDNS,CN=System,";
            string domainDNSZonesTag = "DC=DomainDNSZones,";
            string forestDNSZonesTag = "DC=ForestDNSZones,";
            string lostAndFoundTag = "CN=LostAndFound,";

            if (dn.IndexOf(microsoftDnsTag, StringComparison.OrdinalIgnoreCase) > -1) {
                domainDN = dn.Substring(dn.IndexOf(microsoftDnsTag, StringComparison.OrdinalIgnoreCase) + microsoftDnsTag.Length);
            }
            // handle case for DC=<formerName>,CN=LostAndFound,DC=contoso,DC=com
            else if (dn.IndexOf(lostAndFoundTag, StringComparison.OrdinalIgnoreCase) > -1) {
                domainDN = dn.Substring(dn.IndexOf(lostAndFoundTag, StringComparison.OrdinalIgnoreCase) + lostAndFoundTag.Length);
            }
            // handle case for DomainDNSZones
            else if (dn.IndexOf(domainDNSZonesTag, StringComparison.OrdinalIgnoreCase) > -1) {
                domainDN = dn.Substring(dn.IndexOf(domainDNSZonesTag, StringComparison.OrdinalIgnoreCase) + domainDNSZonesTag.Length);
            }
            // handle case for ForestDNSZones
            else if (dn.IndexOf(forestDNSZonesTag, StringComparison.OrdinalIgnoreCase) > -1) {
                domainDN = dn.Substring(dn.IndexOf(forestDNSZonesTag, StringComparison.OrdinalIgnoreCase) + forestDNSZonesTag.Length);
            }
            else {
                domainDN = dn.Substring(dn.IndexOf("DC=", StringComparison.OrdinalIgnoreCase));
            }

            return domainDN;
        }

        [DebuggerStepThroughAttribute]
        public static string GetNameFromCanonicalName(string canonicalName) {
            if (string.IsNullOrWhiteSpace(canonicalName)) return null;
            string name = null;
            canonicalName = canonicalName.Trim();

            if ((canonicalName.IndexOf("/") > 0) && (canonicalName.LastIndexOf("/") + 1 < canonicalName.Length)) {
                name = canonicalName.Substring(canonicalName.LastIndexOf("/") + 1);
            }
            return name;
        }

        public static string GetParentDN(string dn) {
            string parentDN = null;

            if (!string.IsNullOrWhiteSpace(dn)) {
                parentDN = dn.Trim();

                if (parentDN.StartsWith("CN=", StringComparison.OrdinalIgnoreCase) || parentDN.StartsWith("OU=", StringComparison.OrdinalIgnoreCase) || parentDN.StartsWith("DC=", StringComparison.OrdinalIgnoreCase)) {
                    parentDN = parentDN.Substring(3);
                }

                var nextElementIndexes = new List<int>();
                nextElementIndexes.Add(parentDN.IndexOf(",CN=", StringComparison.OrdinalIgnoreCase));
                nextElementIndexes.Add(parentDN.IndexOf(",DC=", StringComparison.OrdinalIgnoreCase));
                nextElementIndexes.Add(parentDN.IndexOf(",OU=", StringComparison.OrdinalIgnoreCase));

                var nextElementIndex = nextElementIndexes.Where(x => x > -1).Min();

                if ((nextElementIndex > 0) && (parentDN.Length > nextElementIndex + 1)) {
                    parentDN = parentDN.Substring(nextElementIndex + 1);
                }
            }

            return parentDN;
        }

        /// <summary>
        /// If a DN specifies a server name (server bind), return the server name.
        /// </summary>
        public static string GetServerFromLdapDN(string ldapDN) {
            if (string.IsNullOrWhiteSpace(ldapDN)) {

            }
            string dcDnsName = string.Empty;
            string ldapDNSubstring = ldapDN;

            if (ldapDNSubstring.ToUpper().Contains("DC=")) {
                if (ldapDNSubstring.StartsWith("LDAP://", StringComparison.OrdinalIgnoreCase)) {
                    ldapDNSubstring = ldapDNSubstring.Substring("LDAP://".Length);
                }
                else if (ldapDNSubstring.StartsWith("GC://", StringComparison.OrdinalIgnoreCase)) {
                    ldapDNSubstring = ldapDNSubstring.Substring("GC://".Length);
                }

                if (ldapDNSubstring.IndexOf("/") > -1) {
                    dcDnsName = ldapDNSubstring.Substring(0, ldapDNSubstring.IndexOf("/"));
                }
            }

            return dcDnsName;
        }

        public static string ParseFilterElement(string filterElement) {
            return ADSearchFilterEncode(filterElement);
        }
        #endregion

        public static string ToOctetString(Guid guid) {
            return guid.ToOctectString();
        }

        /// <summary>
        /// Encodes disallowed characters of a string used for LDAP distinguished names
        /// </summary>
        /// <param name="text">Reference String</param>
        /// <returns>Encoded String</returns>
        /// <![CDATA[
        /// Active Directory requires that the following characters be escaped with the backslash '\'
        /// Comma ','
        /// Backslash '\'
        /// Forward slash '/'
        /// Pound sign (hash sign) '#'
        /// Plus sign '+'
        /// Less than symbol '<'
        /// Greater than symbol '>'
        /// Semicolon ';'
        /// Double quote '"'
        /// Equal sign '='
        /// Leading or trailing spaces in a component name '("CN= fark ,'
        /// ]]>
        [DebuggerStepThroughAttribute]
        public static string DNEncode(string text) {
            if (string.IsNullOrWhiteSpace(text)) return text;

            var encodedText = new StringBuilder(text.Length);
            var okCharacterLocations = new List<int>();
            foreach (string filter in DNAllowedStrings) {
                okCharacterLocations.AddRange(GetTextLocations(filter, text));
            }

            foreach (string filter in DNEscapedCharacterMap.Values) {
                okCharacterLocations.AddRange(GetTextLocations(filter, text));
            }

            foreach (string filter in DNAlternateEscapedCharacters) {
                okCharacterLocations.AddRange(GetTextLocations(filter, text));
            }

            okCharacterLocations = okCharacterLocations.Distinct().ToList();

            for (int index = 0; index < text.Length; index++) {
                var currentCharacter = text[index];
                if (okCharacterLocations.Any(x => x == index)) {
                    encodedText.Append(currentCharacter);
                }
                else {
                    if (DNEscapedCharacterMap.ContainsKey(text[index])) {
                        encodedText.Append(DNEscapedCharacterMap[currentCharacter]);
                    }
                    else {
                        encodedText.Append(currentCharacter);
                    }
                }
            } // for (int index = 0; index < text.Length; index++)

            return encodedText.ToString();
        }

        [DebuggerStepThroughAttribute]
        public static string AdjustADSearchFilter(string text) {
            string adjustedFilter = text;
            if (text.Count(x => x.CompareTo('(') == 0) > 1) {
                if (!text.StartsWith("(&") && !text.StartsWith("(|")) {
                    adjustedFilter = string.Format("(&{0})", text);
                }
            }

            return adjustedFilter;
        }

        /// <summary>
        /// Checks for the attributes required for each query, and adds them if not present
        /// </summary>
        /// <param name="attributes"></param>
        /// <returns>A string array of attributes</returns>
        [DebuggerStepThroughAttribute]
        private static string[] AddRequiredQueryAttributes(string[] attributes) {
            if (attributes == null) {
                throw new ArgumentNullException("attributes");
            }

            var updatedAttributes = new List<string>();
            updatedAttributes.AddRange(attributes);
            foreach (var requiredAttribute in RequiredQueryAttributes) {
                if (!updatedAttributes.Any(x => string.Equals(x, requiredAttribute, StringComparison.OrdinalIgnoreCase))) {
                    updatedAttributes.Add(requiredAttribute);
                }
            }

            return updatedAttributes.ToArray();
        }

        /// <summary>
        /// Replaces left and right parentheses with the appropriate escape numeric value
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        [DebuggerStepThroughAttribute]
        public static string ADSearchFilterEncode(string text) {
            var encodedFilter = new StringBuilder();

            for (int index = 0; index < text.Length; index++) {
                char currentCharacter = text[index];
                switch (currentCharacter) {
                    case '\\':
                        encodedFilter.Append(@"\5c");
                        break;
                    case '*':
                        encodedFilter.Append(@"\2a");
                        break;
                    case '(':
                        encodedFilter.Append(@"\28");
                        break;
                    case ')':
                        encodedFilter.Append(@"\29");
                        break;
                    case '\u0000':
                        encodedFilter.Append(@"\00");
                        break;
                    case '/':
                        encodedFilter.Append(@"\2f");
                        break;
                    default:
                        encodedFilter.Append(currentCharacter);
                        break;
                }
            }

            return encodedFilter.ToString();
        }

        /// <summary>
        /// Gets the Distinguished Names of the Group Policies that are in a gpLink attribute
        /// </summary>
        /// <param name="gpLink">The gpLink attribute value</param>
        /// <returns>The collection of Group Policy Objecgt Distingushed Names</returns>
        [DebuggerStepThroughAttribute]
        public static List<string> GetGPLinkGPODNs(string gpLink) {
            var gpoDNs = new List<string>();

            if (!string.IsNullOrWhiteSpace(gpLink)) {
                string gpLinkTemp = gpLink;

                // remove invalid gpLinks
                gpLinkTemp = gpLinkTemp.Replace("[LDAP://;0]", string.Empty);

                while ((gpLinkTemp.IndexOf("CN=", StringComparison.OrdinalIgnoreCase) > -1) && (gpLinkTemp.IndexOf(";", StringComparison.OrdinalIgnoreCase) > -1)) {
                    if (gpLinkTemp.IndexOf(";", StringComparison.OrdinalIgnoreCase) < gpLinkTemp.IndexOf("CN=", StringComparison.OrdinalIgnoreCase)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Warning: gpLink may contain invalid data: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                            CustomExtensions.CurrentMethodName(), gpLink));
                        break;
                    }

                    int dnStartIndex = gpLinkTemp.IndexOf("CN=", StringComparison.OrdinalIgnoreCase);
                    int dnEndIndex = gpLinkTemp.IndexOf(";", StringComparison.OrdinalIgnoreCase);
                    string gpoDN = gpLinkTemp.Substring(dnStartIndex, (dnEndIndex - dnStartIndex));

                    gpoDNs.Add(gpoDN);
                    gpLinkTemp = gpLinkTemp.Substring(dnEndIndex);
                    if (gpLinkTemp.IndexOf("[", StringComparison.OrdinalIgnoreCase) > -1) {
                        gpLinkTemp = gpLinkTemp.Substring(gpLinkTemp.IndexOf("[", StringComparison.OrdinalIgnoreCase));
                    }
                    else {
                        break;
                    }
                }
            }

            return gpoDNs;
        }

        /// <summary>
        /// Gets the ObjectCategory and ObjectClass, and adds them to the search filter
        /// </summary>
        /// <typeparam name="T">The type of object</typeparam>
        /// <param name="filter">The search filter</param>
        /// <returns>The adjusted search filter</returns>
        [DebuggerStepThroughAttribute]
        public static string GetObjectClassObjectCategoryForSearchFilter<T>(string filter) where T : ADBaseObjectProperties {

            // objectClass is useful when it is different from the category, such as with user objects
            var objectClassMissing = string.IsNullOrWhiteSpace(filter) || !Regex.IsMatch(filter, "objectClass=", RegexOptions.IgnoreCase);
            var objectCategoryMissing = string.IsNullOrWhiteSpace(filter) || !Regex.IsMatch(filter, "objectCategory=", RegexOptions.IgnoreCase);
            if (string.IsNullOrWhiteSpace(filter) || objectClassMissing || objectCategoryMissing) {

                #region Get objectClass and objectCategory short name
                string objectClassShortName = string.Empty;
                FieldInfo objectClassField = typeof(T).GetField("ObjectClassShortName");

                if (objectClassField != null) {
                    objectClassShortName = objectClassField.GetValue(typeof(T)) as string;
                }

                string objectCategoryShortName = string.Empty;
                FieldInfo objectCategoryField = typeof(T).GetField("ObjectCategoryShortName");

                if (objectCategoryField != null) {
                    objectCategoryShortName = objectCategoryField.GetValue(typeof(T)) as string;
                }
                #endregion

                // if we are missing objectClass, but objectCategory is present
                if ((objectClassMissing && !string.IsNullOrWhiteSpace(objectClassShortName)) && !objectCategoryMissing) {
                    if (!string.IsNullOrWhiteSpace(filter)) {
                        filter = string.Format("(&(objectClass={0}){1})", objectClassShortName, filter);
                    }
                    else {
                        filter = string.Format("(objectClass={0})", objectClassShortName);
                    }
                }
                // if we are missing objectClass and objectCategory
                else if (objectClassMissing && objectCategoryMissing) {
                    //objectClass and objectCategory
                    if (!string.IsNullOrWhiteSpace(objectClassShortName) && !string.IsNullOrWhiteSpace(objectCategoryShortName)) {
                        // note that deleted objects do not have an object category
                        if (string.IsNullOrWhiteSpace(filter) || !Regex.IsMatch(filter, "isDeleted", RegexOptions.IgnoreCase)) {
                            if (!string.IsNullOrWhiteSpace(filter)) {
                                filter = string.Format("(&(objectCategory={0})(objectClass={1}){2})", objectCategoryShortName, objectClassShortName, filter);
                            }
                            else {
                                filter = string.Format("(&(objectCategory={0})(objectClass={1}))", objectCategoryShortName, objectClassShortName);
                            }
                        }
                        else if (!string.IsNullOrWhiteSpace(filter)) {
                            filter = string.Format("(&(objectClass={0}){1})", objectClassShortName, filter);
                        }
                        else {
                            filter = string.Format("(objectClass={0})", objectClassShortName);
                        }
                    }
                    // objectCategory only
                    else if (!string.IsNullOrWhiteSpace(objectCategoryShortName)) {
                        // note that deleted objects do not have an object category
                        if (string.IsNullOrWhiteSpace(filter) || !Regex.IsMatch(filter, "isDeleted", RegexOptions.IgnoreCase)) {
                            if (!string.IsNullOrWhiteSpace(filter)) {
                                filter = string.Format("(&(objectCategory={0}){1})", objectCategoryShortName, filter);
                            }
                            else {
                                filter = string.Format("(objectCategory={0})", objectCategoryShortName);
                            }
                        }
                    }
                }
            }

            return filter;
        }

        /// <summary>
        /// Returns the integer indexes of the locations where a specified substring exists in the supplied string.
        /// </summary>
        [DebuggerStepThroughAttribute]
        public static List<int> GetTextLocations(string filter, string text) {
            var locations = new List<int>();
            int location = 0;
            do {
                location = text.IndexOf(filter, location, StringComparison.OrdinalIgnoreCase);
                if (location > -1) {
                    locations.Add(location);
                    location++;
                    for (int index = 1; index < filter.Length; index++, location++) {
                        locations.Add(location);
                    }
                }
            } while (location > -1);

            return locations;
        }

        /// <summary>
        /// Gets the contents of a multi-valued attribute collection of the specified type
        /// </summary>
        /// <typeparam name="T">The specified type of the value collection</typeparam>
        /// <param name="propertyName">The attribute name that contains the multi-valued collection</param>
        /// <param name="attributes">The attribute multi-valued collection.  Key: the target attribute name Value: multi-valued collection</param>
        /// <returns></returns>
        public static List<T> GetMultiValuedAttribute<T>(string propertyName, Dictionary<string, object[]> attributes) where T : class {
            var list = new List<T>();
            if (attributes.ContainsKey(propertyName)) {
                for (int index = 0; index < attributes[propertyName].Length; index++) {
                    var attributeValue = attributes[propertyName][index] as T;
                    if (attributeValue != null) {
                        list.Add(attributeValue);
                    }
                }
            }

            return list;
        }

        #endregion

        #region AccessRule methods
        public static bool RemoveAccessRuleSpecfic(string objectDN, ActiveDirectoryAccessRule accessRule) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(objectDN) ? "NULL" : objectDN));

            #region Validation
            if (string.IsNullOrWhiteSpace(objectDN)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", objectDN != null ? objectDN : "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = objectDN,
                AccessRule = accessRule
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        ActiveDirectorySecurity activeDirectorySecurity = directoryEntry.ObjectSecurity;
                        activeDirectorySecurity.RemoveAccessRuleSpecific(methodParameters.AccessRule);

                        directoryEntry.CommitChanges();
                        return true;
                    }
                });
        }
        /// <summary>
        /// Adds an AccessRule to an ActiveDirectory object as specified by the DN
        /// </summary>
        /// <remarks>Example
        ///     PropertyAccessRule denyWriteUacAccessRule = new PropertyAccessRule(
        ///        new NTAccount("DOMAIN", "username"),
        ///        AccessControlType.Deny,
        ///        PropertyAccess.Write,
        ///        new Guid("BF967A68-0DE6-11D0-A285-00AA003049E2"));
        /// </remarks>
        public static bool AddAccessRule(string objectDN, ActiveDirectoryAccessRule accessRule) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(objectDN) ? "NULL" : objectDN));

            #region Validation
            if (string.IsNullOrWhiteSpace(objectDN)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", objectDN ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = objectDN,
                AccessRule = accessRule
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        ActiveDirectorySecurity activeDirectorySecurity = directoryEntry.ObjectSecurity;
                        activeDirectorySecurity.AddAccessRule(accessRule);

                        directoryEntry.CommitChanges();
                        return true;
                    }
                });
        }
        public static bool SetOwner(string objectDN, IdentityReference identity) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(objectDN) ? "NULL" : objectDN));

            #region Validation
            if (string.IsNullOrWhiteSpace(objectDN)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", objectDN ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = objectDN,
                Identity = identity
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        ActiveDirectorySecurity activeDirectorySecurity = directoryEntry.ObjectSecurity;
                        activeDirectorySecurity.SetOwner(methodParameters.Identity);

                        directoryEntry.CommitChanges();
                        return true;
                    }
                });
        }
        public static List<string> GetAccessRulesFriendly(string sddl) {
            var accessRuleInfo = new List<string>();
            var activeDirectorySecurity = new ActiveDirectorySecurity();
            activeDirectorySecurity.SetSecurityDescriptorSddlForm(sddl);

            foreach (ActiveDirectoryAccessRule accessRule in activeDirectorySecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(SecurityIdentifier))) {
                NTAccount ntaccount = accessRule.IdentityReference.Translate(typeof(NTAccount)) as NTAccount;
                accessRuleInfo.Add(string.Format("Principal: {0}; SID: {1}; Allow/Deny: {2}; Inherited: {3}; Propagation Flags: {4} Access: {5}",
                    ntaccount != null ? ntaccount.Value : "N/A",
                    accessRule.IdentityReference.Value,
                    accessRule.AccessControlType.ToString(),
                    accessRule.IsInherited,
                    accessRule.PropagationFlags.ToString(),
                    accessRule.ActiveDirectoryRights.ToString()));
            }

            return accessRuleInfo;
        }
        public static AuthorizationRuleCollection GetAccessRules(string dn) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(dn) ? "NULL" : dn));

            #region Validation
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", dn ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, AuthorizationRuleCollection>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(methodParameters.ObjectDN, PartitionScope.LocalPartition);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return null;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        ActiveDirectorySecurity adSecurity = directoryEntry.ObjectSecurity;
                        return adSecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(SecurityIdentifier));
                    }

                });
        }
        public static ActiveDirectorySecurity GetObjectSecurity(string dn) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(dn) ? "NULL" : dn));

            #region Validation
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", dn ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, ActiveDirectorySecurity>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(methodParameters.ObjectDN, PartitionScope.LocalPartition);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return null;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        return directoryEntry.ObjectSecurity;
                    }
                });
        }
        public static bool HasAccessRule(string dn, ActiveDirectoryAccessRule accessRuleToCheck) {
            return HasAccessRule(GetAccessRules(dn), accessRuleToCheck);
        }
        public static bool HasAccessRule(AuthorizationRuleCollection accessRules, ActiveDirectoryAccessRule accessRuleToCheck) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            if (accessRules == null) {
                throw new ArgumentNullException("accessRules");
            }
            if (accessRuleToCheck == null) {
                throw new ArgumentNullException("accessRuleToCheck");
            }

            bool hasAccessRule = false;

            var accessRuleToCheckSID = accessRuleToCheck.IdentityReference as SecurityIdentifier;
            if (accessRuleToCheckSID == null) return false;

            foreach (ActiveDirectoryAccessRule accessRule in accessRules) {
                if (accessRule.AccessControlType == accessRuleToCheck.AccessControlType) {
                    var accessRuleSID = accessRule.IdentityReference as SecurityIdentifier;
                    if (accessRuleSID == null) continue;
                    if (accessRuleSID == accessRuleToCheckSID) {
                        if (accessRule.ActiveDirectoryRights == accessRuleToCheck.ActiveDirectoryRights) {
                            if (accessRule.InheritanceType == accessRuleToCheck.InheritanceType) {
                                if (accessRule.InheritanceFlags == accessRuleToCheck.InheritanceFlags) {
                                    if (accessRule.InheritedObjectType == accessRuleToCheck.InheritedObjectType) {
                                        if (accessRule.ObjectType == accessRuleToCheck.ObjectType) {
                                            if (accessRule.PropagationFlags == accessRuleToCheck.PropagationFlags) {
                                                hasAccessRule = true;
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return hasAccessRule;
        }
        public static bool IsProtectedFromDelete(string dn) {
            return IsProtectedFromDelete(GetAccessRules(dn));
        }
        public static bool IsProtectedFromDelete(AuthorizationRuleCollection accessRules) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            return HasAccessRule(accessRules, ProtectedFromDeleteAccessRule);

        }
        #endregion

        #region Object Move, Update, Delete methods
        /// <summary>
        /// Clears the value of an attribute for an object, effectively removing the attribute from the object
        /// </summary>
        /// <param name="dn">The distinguished name of the object</param>
        /// <param name="attributeName">The name of the attribute to clear</param>
        /// <returns>True if successful</returns>
        public static bool ClearAttribute(string dn, string attributeName) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} attributeName: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                dn == null ? "NULL" : dn, attributeName == null ? "NULL" : attributeName));

            #region Validation
            if (string.IsNullOrWhiteSpace(attributeName)) {
                throw new ApplicationException(string.Format("Invalid attribute name: {0}", attributeName ?? "NULL"));
            }
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", dn ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn,
                AttributeName = attributeName
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        bool attributeExists = directoryEntry.Properties.Contains(attributeName);
                        if (attributeExists) {
                            directoryEntry.Properties[attributeName].Clear();
                            directoryEntry.CommitChanges();
                        }

                        return true;
                    }
                });
        }
        /// <summary>
        /// Delete an Active Directory object
        /// </summary>
        /// <param name="DNs"></param>
        /// <returns></returns>
        public static bool Delete(IEnumerable<string> DNs) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            bool status = true;
            foreach (string dn in DNs) {
                status &= Delete(dn);
            }

            return status;
        }
        public static bool Delete(string dn) {
            return Delete(dn, deleteCriticalSystemObject: false, deleteChildren: true);
        }
        public static bool Delete(string dn, bool deleteCriticalSystemObject, bool deleteChildren) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} for DN: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(dn) ? "NULL" : dn));

            #region Validation
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", dn ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn,
                DeleteCriticalSystemObject = deleteCriticalSystemObject,
                DeleteChildren = deleteChildren
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        return ADDataAccess.Delete(directoryEntry, methodParameters.DeleteCriticalSystemObject, methodParameters.DeleteChildren);
                    }
                });
        }
        public static bool Delete(DirectoryEntry directoryEntry, bool deleteCriticalSystemObject, bool deleteChildren) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} for DirectoryEntry DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                directoryEntry != null ? directoryEntry.Path : "NULL"));

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDirectoryEntry = directoryEntry,
                DeleteCriticalSystemObject = deleteCriticalSystemObject,
                DeleteChildren = deleteChildren
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = methodParameters.ObjectDirectoryEntry.Path;
                    if (!ObjectExists(methodParameters.ObjectDirectoryEntry.Path)) return true;

                    var parentDirectoryEntryAuthenticationType = GetAdjustedAuthenticationType(methodParameters.ObjectDirectoryEntry.Parent.Path);
                    using (var parentDirectoryEntry = new DirectoryEntry(methodParameters.ObjectDirectoryEntry.Parent.Path, null, null, parentDirectoryEntryAuthenticationType)) {
                        if (methodParameters.ObjectDirectoryEntry.Properties.Contains("isCriticalSystemObject")) {
                            bool isCriticalSystemObject = Convert.ToBoolean(methodParameters.ObjectDirectoryEntry.Properties["isCriticalSystemObject"][0]);
                            if (isCriticalSystemObject && !methodParameters.DeleteCriticalSystemObject) return false;
                        }

                        if (deleteChildren) {
                            // may need to delete child objects of this object.  E.g., Computers may have "printer" child objects.
                            var childDirectoryEntries = new List<DirectoryEntry>();

                            foreach (DirectoryEntry childDirectoryEntry in methodParameters.ObjectDirectoryEntry.Children) {
                                childDirectoryEntries.Add(childDirectoryEntry);
                            }

                            for (int index = 0; index < childDirectoryEntries.Count; index++) {
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Removing child DN: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                                    CustomExtensions.CurrentMethodName(), childDirectoryEntries[index].Path));

                                ADDataAccess.Delete(childDirectoryEntries[index], methodParameters.DeleteCriticalSystemObject, methodParameters.DeleteChildren);
                            }
                        }

                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Removing object DN: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                            CustomExtensions.CurrentMethodName(), methodParameters.ObjectDirectoryEntry.Path));

                        parentDirectoryEntry.Children.Remove(methodParameters.ObjectDirectoryEntry);

                        parentDirectoryEntry.CommitChanges();
                        parentDirectoryEntry.RefreshCache();
                    }

                    return true;
                });
        }

        /// <summary>
        /// Moves an object from a source Organizational Unit to a destination Organizational Unit
        /// </summary>
        /// <param name="sourceObjectDN">The source object distinguished name</param>
        /// <param name="destinationLocationDN">The destination container distinguished name</param>
        /// <returns>True if successful</returns>
        public static bool MoveObject(string sourceObjectDN, string destinationLocationDN) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} sourceDN: {3} destinationLocationDN: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                string.IsNullOrWhiteSpace(sourceObjectDN) ? "NULL" : sourceObjectDN, string.IsNullOrWhiteSpace(destinationLocationDN) ? "NULL" : destinationLocationDN));

            #region Validate source
            if (!ObjectExists(sourceObjectDN)) {
                throw new ApplicationException(string.Format("Source object does not exist: {0}", sourceObjectDN ?? "NULL"));
            }

            var sourceDomainInfo = ADDomainInfo
                .Where(x => sourceObjectDN.EndsWith(x.DistinguishedName, StringComparison.OrdinalIgnoreCase))
                .FirstOrDefault();

            var destinationDomainInfo = ADDomainInfo
                .Where(x => destinationLocationDN.EndsWith(x.DistinguishedName, StringComparison.OrdinalIgnoreCase))
                .FirstOrDefault();

            if (sourceDomainInfo != destinationDomainInfo) {
                throw new ApplicationException(string.Format("Source: {0} is not in same domain as destination: {1}",
                    sourceObjectDN != null ? sourceObjectDN : "NULL", destinationLocationDN != null ? destinationLocationDN : "NULL"));
            }
            #endregion

            MethodParameters methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                SourceObjectDN = sourceObjectDN,
                DestinationLocationDN = destinationLocationDN
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    if (!ObjectExists(methodParameters.SourceObjectDN)) return false;

                    using (var destinationDirectoryEntry = ADDataAccess.OUMethods.CreateOU(methodParameters.DestinationLocationDN)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} destinationDirectoryEntry: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                            destinationDirectoryEntry != null ? destinationDirectoryEntry.Path : "NULL"));

                        if (destinationDirectoryEntry == null) return false;

                        methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.DestinationLocationDN);

                        string destinationLocationPrefix = destinationDirectoryEntry.Path.Substring(0, destinationDirectoryEntry.Path.IndexOf("OU="));
                        string sourceDNAdjustedBindPath = destinationLocationPrefix + methodParameters.SourceObjectDN;

                        using (var objectToMoveDirectoryEntry = new DirectoryEntry(sourceDNAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {
                            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} objectToMoveDirectoryEntry: {3}",
                                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                                objectToMoveDirectoryEntry != null ? objectToMoveDirectoryEntry.Path : "NULL"));

                            objectToMoveDirectoryEntry.MoveTo(destinationDirectoryEntry);
                            objectToMoveDirectoryEntry.CommitChanges();

                            return true;
                        }
                    }
                });
        }

        /// <summary>
        /// Appends a range of values to a multi-valued attribute.
        /// </summary>
        /// <param name="dn">The DN of the object where the attribute exists.</param>
        /// <param name="attributeName">The name of the multi-valued attribute.</param>
        /// <param name="values">The array of values.</param>
        /// <returns>True if successful</returns>
        public static bool AppendMutliValuedAttribute(string dn, string attributeName, object[] values) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} attributeName: {4} value count: {5}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                dn ?? "NULL", attributeName ?? "NULL", values == null ? "NULL" : values.Length.ToString()));

            #region Validation
            if (string.IsNullOrWhiteSpace(attributeName)) {
                throw new ArgumentNullException("attributeName");
            }
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }

            if (!ADSchemaAttributes.ContainsKey(attributeName)) {
                throw new ArgumentException(string.Format("Attribute name not found in schema: {0}", attributeName), "attributeName");
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn,
                AttributeName = attributeName,
                AttributeValueArray = values
            };

            #region Validation
            SchemaAttribute adAttribute = ADSchemaAttributes[methodParameters.AttributeName];
            if (adAttribute.IsSingleValued.HasValue && adAttribute.IsSingleValued.Value) {
                throw new ArgumentException(string.Format("Attribute is not multivalued: {0}", attributeName), "attributeName");
            }
            #endregion

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {

                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {
                        directoryEntry.Properties[methodParameters.AttributeName].AddRange(methodParameters.AttributeValueArray);
                        directoryEntry.CommitChanges();
                    }

                    return true;
                });

        }

        /// <summary>
        /// Use this to ensure that the values of a multi-valued string attribute are replaced, not added to.
        /// </summary>
        /// <param name="dn">dn of object to update</param>
        /// <param name="attributeName">The attribute name</param>
        /// <param name="values">The collection of attribute values</param>
        public static bool ReplaceMutliValuedAttribute(string dn, string attributeName, IEnumerable<object> values) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} attributeName: {4} value count: {5}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                dn ?? "NULL", attributeName ?? "NULL", values == null ? "NULL" : values.Count().ToString()));

            bool success = false;
            if (ADDataAccess.ClearAttribute(dn, attributeName)) {
                success = true;

                if ((values != null) && (values.Count() > 0)) {
                    foreach (var attributeValue in values) {
                        success &= ADDataAccess.UpdateAttribute(dn, attributeName, attributeValue);
                    }
                }
            }

            return success;
        }

        /// <summary>
        /// Updates the value of an attribute on the specified object
        /// </summary>
        /// <param name="dn">dn of object to update</param>
        /// <param name="attributeName">The attribute name</param>
        /// <param name="value">The attribute value. Must be single-valued (not an array).</param>
        /// <remarks>Warning: checking for the existence of an attribute is case-sensitive</remarks>
        public static bool UpdateAttribute(string dn, string attributeName, object value) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} attributeName: {4} value: {5}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                dn ?? "NULL", attributeName ?? "NULL", value ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(attributeName)) {
                throw new ArgumentNullException("attributeName");
            }
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }

            if (!ADSchemaAttributes.ContainsKey(attributeName)) {
                throw new ArgumentException(string.Format("Attribute name not found in schema: {0}", attributeName ?? "NULL"), "attributeName");
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn,
                AttributeName = attributeName,
                AttributeValue = ((value == null) || (value.ToString().Length == 0)) ? null : value
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    bool attributeUpdated = false;

                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(forupdate: true, bindPath: methodParameters.ObjectDN);
                    if (!ObjectExists(methodParameters.ServerNameOrAdjustedBindPath)) return false;

                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes)) {

                        bool attributeExists = directoryEntry.Properties.Contains(methodParameters.AttributeName);
                        SchemaAttribute adAttribute = ADSchemaAttributes[methodParameters.AttributeName];

                        // if attribute exists and new value == null, clear attribute
                        // else if multivalued attribute
                        //  -> .Add
                        // else if single-valued
                        // -> .Value = 

                        if (attributeExists && (methodParameters.AttributeValue == null)) {
                            directoryEntry.Properties[methodParameters.AttributeName].Clear();
                            attributeUpdated = true;
                        }
                        else if (methodParameters.AttributeValue != null) {


                            if ((adAttribute.IsSingleValued.HasValue && (adAttribute.IsSingleValued.Value == false)) || !attributeExists) {
                                // .add() for multivalued or new attributes
                                // TODO: Why .Add for new attributes/check for not exists?

                                directoryEntry.Properties[methodParameters.AttributeName].Add(methodParameters.AttributeValue);
                                attributeUpdated = true;
                            }
                            else {
                                // .value = for single valued attributes
                                directoryEntry.Properties[methodParameters.AttributeName].Value = methodParameters.AttributeValue;
                                attributeUpdated = true;
                            }
                        }

                        if (attributeUpdated) {
                            directoryEntry.CommitChanges();
                        }

                        return true;
                    } // using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath)) {
                });
        }

        /// <summary>
        /// Removes a lingering object using the newer syntax described in KB14282
        /// <GUID=[writeable DC NTDSSettings object objectGuid for the partition where the object exists]> : <GUID=[objectGuid of object to remove]> 
        /// https://support.microsoft.com/en-us/kb/314282
        /// </summary>
        /// <param name="targetDomainControllerFqdn">The domain controller with the lingering object to remove</param>
        /// <param name="objectGuidToRemove">The objectGuid of the lingering object to remove</param>
        /// <param name="objectPartitionDN">The partition where the object resides. This is usually availble in the event id 1988 message.</param>
        public static void RemoveLingeringObject(string targetDomainControllerFqdn, Guid objectGuidToRemove, string objectPartitionDN) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} targetDomainControllerTqdn: {3} objectGuidToRemove: {4} partitionDN: {5}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                targetDomainControllerFqdn ?? "NULL", objectGuidToRemove, objectPartitionDN ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(targetDomainControllerFqdn)) {
                throw new ArgumentNullException("targetDomainControllerTqdn");
            }
            if (targetDomainControllerFqdn.IndexOf(".") == -1) {
                throw new ArgumentOutOfRangeException("targetDomainControllerFqdn does not contain '.'");
            }
            if (targetDomainControllerFqdn.IndexOf(".") == targetDomainControllerFqdn.LastIndexOf(".")) {
                throw new ArgumentOutOfRangeException("targetDomainControllerFqdn does not contain two '.'");
            }
            if (objectGuidToRemove == Guid.Empty) {
                throw new ArgumentOutOfRangeException("objectGuidToRemove");
            }
            if (string.IsNullOrWhiteSpace(objectPartitionDN)) {
                throw new ArgumentNullException("partitionDN");
            }
            #endregion

            try {

                #region Get object partition DomainInfo
                var objectPartitionDomainDN = GetDomainDNFromDN(objectPartitionDN);
                #region Validation for objectPartitionDomainDN
                if (string.IsNullOrWhiteSpace(objectPartitionDomainDN)) {
                    throw new ApplicationException(string.Format("Unable to determine DomainDN from objectPartitionDN: {0}", objectPartitionDN));
                }
                #endregion

                var objectPartitionDomainInfo = ADDataAccess.ADDomainInfo
                    .Where(x => string.Equals(x.DistinguishedName, objectPartitionDomainDN, StringComparison.OrdinalIgnoreCase))
                    .FirstOrDefault();
                #region Validation for objectPartitionDomainInfo
                if (objectPartitionDomainInfo == null) {
                    throw new ApplicationException(string.Format("Unable to locate DomainInfo object for objectPartitionDomainDN: {0}", objectPartitionDomainDN));
                }
                #endregion
                #endregion

                #region Get object partition PDC Emulator (could be any writable DC)
                var objectPartitionPDCESiteServerFilter = string.Format("(dnsHostName={0})", objectPartitionDomainInfo.PDCEmulatorDnsName);
                var objectPartitionPDCESiteServerSiteServer = ADDataAccess.GetADObjectProperties<SiteServer>(
                    ADDataAccess.ForestRootConfigurationNamingContext, SiteServer.AttributesToGet, objectPartitionPDCESiteServerFilter, PartitionScope.LocalPartition)
                    .FirstOrDefault();
                #region Validate SiteServer for objectPartitionDomainInfo.PDCEmulatorDnsName
                if (objectPartitionPDCESiteServerSiteServer == null) {
                    throw new ApplicationException(string.Format("Unable to find SiteServer for objectPartitionDomainInfo.PDCEmulatorDnsName: {0}", objectPartitionDomainInfo.PDCEmulatorDnsName));
                }
                if (objectPartitionPDCESiteServerSiteServer.NTDSSettings == null) {
                    throw new ApplicationException(string.Format("SiteServer: {0} does not have an NTDSSettings object", objectPartitionPDCESiteServerSiteServer.DN));
                }
                #endregion
                #endregion

                #region Determine if object to delete is in the local or global catalog (read-only) partition
                var targetDomainControllerDNSDomain = targetDomainControllerFqdn.Substring(targetDomainControllerFqdn.IndexOf("."));
                var targetDomainControllerDomainInfo = ADDataAccess.ADDomainInfo
                    .Where(x => string.Equals(x.DNSName, targetDomainControllerDNSDomain, StringComparison.OrdinalIgnoreCase))
                    .FirstOrDefault();
                #region Validation for targetDomainControllerDomainInfo
                if (targetDomainControllerDomainInfo == null) {
                    throw new ApplicationException(string.Format("Unable to get DomainInfo for targetDomainControllerDNSDomain: {0}", targetDomainControllerDNSDomain));
                }
                #endregion

                bool isGCPartition = true;
                if (targetDomainControllerDomainInfo.DNSName == objectPartitionDomainInfo.DNSName) {
                    isGCPartition = false;
                }
                #endregion

                #region Construct bind path based on local or global catalog
                var targetRootDSEPath = string.Empty;
                if (isGCPartition) {
                    targetRootDSEPath = string.Format("LDAP://{0}/RootDSE", targetDomainControllerFqdn);
                }
                else {
                    targetRootDSEPath = string.Format("GC://{0}/RootDSE", targetDomainControllerFqdn);
                } 
                #endregion

                using (var targetDirectoryEntry = new DirectoryEntry(targetRootDSEPath)) {
                    targetDirectoryEntry.AuthenticationType = AuthenticationTypes.ServerBind | AuthenticationTypes.Signing | AuthenticationTypes.Sealing;

                    var attributeName = "removeLingeringObject";
                    string attributeValue = string.Format("<GUID={0}> : <GUID={1}>", objectPartitionPDCESiteServerSiteServer.NTDSSettings.ObjectGuid, objectGuidToRemove);

                    targetDirectoryEntry.Invoke("Put", new object[] { attributeName, attributeValue });
                    targetDirectoryEntry.CommitChanges();
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }
        }
        #endregion

        #region Replication Methods

        /// <summary>
        /// Replicates a single object from the SourceDC to the TargetDC.
        /// </summary>
        /// <param name="targetDomainControllers">Target Domain Controller</param>
        /// <param name="sourceDomainControllerFqdn">Source Domain Controller</param>
        /// <param name="objectDNToReplicate">The DN of the object to replicate.</param>
        /// <returns>Replication Status</returns>
        /// <seealso cref="https://msdn.microsoft.com/en-us/library/cc223306.aspx"/>
        public static void ReplicateSingleObject(List<string> targetDomainControllers, string sourceDomainControllerFqdn, string objectDNToReplicate) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            #region Validation
            if (string.IsNullOrWhiteSpace(sourceDomainControllerFqdn)) {
                throw new ArgumentNullException("sourceDomainControllerFqdn");
            }
            if (targetDomainControllers == null) {
                throw new ArgumentNullException("targetDomainControllers");
            }
            if (targetDomainControllers.Count == 0) {
                throw new ArgumentException("targetDomainControllers has zero targets.");
            }
            if (string.IsNullOrWhiteSpace(objectDNToReplicate)) {
                throw new ArgumentNullException("objectDNToReplicate");
            }
            #endregion

            try {
                foreach (var targetDomainController in targetDomainControllers) {
                    ReplicateSingleObject(targetDomainController, sourceDomainControllerFqdn, objectDNToReplicate);
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }
        }

        /// <summary>
        /// Forces SingleObjectReplication from the SourceDC to the TargetDC.
        /// </summary>
        /// <param name="targetDomainControllerFqdn">Target Domain Controller</param>
        /// <param name="sourceDomainControllerFqdn">Source Domain Controller</param>
        /// <param name="objectDNToReplicate">The DN of the object to replicate.</param>
        /// <returns>Replication Status</returns>
        /// <seealso cref="https://msdn.microsoft.com/en-us/library/cc223306.aspx"/>
        public static void ReplicateSingleObject(string targetDomainControllerFqdn, string sourceDomainControllerFqdn, string objectDNToReplicate) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} targetDomainControllerFqdn: {3} sourceDomainControllerFqdn: {4} objectDNToReplicate: {5}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                targetDomainControllerFqdn ?? "NULL", sourceDomainControllerFqdn ?? "NULL", objectDNToReplicate ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(sourceDomainControllerFqdn)) {
                throw new ArgumentNullException("sourceDomainControllerFqdn");
            }
            if (sourceDomainControllerFqdn.IndexOf(".") == -1) {
                throw new ArgumentOutOfRangeException("sourceDomainControllerFqdn does not contain '.'");
            }
            if (sourceDomainControllerFqdn.IndexOf(".") == sourceDomainControllerFqdn.LastIndexOf(".")) {
                throw new ArgumentOutOfRangeException("sourceDomainControllerFqdn does not contain two '.'");
            }
            if (string.IsNullOrWhiteSpace(targetDomainControllerFqdn)) {
                throw new ArgumentNullException("targetDomainControllerFqdn");
            }
            if (targetDomainControllerFqdn.IndexOf(".") == -1) {
                throw new ArgumentOutOfRangeException("targetDomainControllerFqdn does not contain '.'");
            }
            if (targetDomainControllerFqdn.IndexOf(".") == targetDomainControllerFqdn.LastIndexOf(".")) {
                throw new ArgumentOutOfRangeException("targetDomainControllerFqdn does not contain two '.'");
            }
            if (string.IsNullOrWhiteSpace(objectDNToReplicate)) {
                throw new ArgumentNullException("objectDNToReplicate");
            }
            #endregion

            try {
                var sourceSiteServerFilter = string.Format("(dnsHostName={0})", sourceDomainControllerFqdn);
                var sourceSiteServer = ADDataAccess.GetADObjectProperties<SiteServer>(
                    ADDataAccess.ForestRootConfigurationNamingContext, SiteServer.AttributesToGet, sourceSiteServerFilter, PartitionScope.LocalPartition)
                    .FirstOrDefault();

                #region Validate SiteServer for sourceDomainControllerFqdn
                if (sourceSiteServer == null) {
                    throw new ApplicationException(string.Format("Unable to find SiteServer for sourceDomainControllerFqdn: {0}", sourceDomainControllerFqdn));
                }
                if (sourceSiteServer.NTDSSettings == null) {
                    throw new ApplicationException(string.Format("SiteServer: {0} does not have an NTDSSettings object", sourceSiteServer.DN));
                }
                #endregion

                var targetRootDSEPath = string.Format("LDAP://{0}/RootDSE", targetDomainControllerFqdn);
                using (var targetDirectoryEntry = new DirectoryEntry(targetRootDSEPath)) {
                    targetDirectoryEntry.AuthenticationType = AuthenticationTypes.ServerBind | AuthenticationTypes.Signing | AuthenticationTypes.Sealing;

                    var attributeName = "replicateSingleObject";
                    string attributeValue = string.Format("{0}:{1}", sourceSiteServer.NTDSSettings.DN, objectDNToReplicate);

                    targetDirectoryEntry.Invoke("Put", new object[] { attributeName, attributeValue });
                    targetDirectoryEntry.CommitChanges();
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }
        }
        #endregion

        #region Ldif Export methods
        /// <summary>
        /// Re-creates objects from LDIFDE export files
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <param name="ldifOutput">The LDIF export file.</param>
        /// <returns>A List of AD objects</returns>
        public static async Task<List<T>> DeserializeObjectsFromLdifAsync<T>(List<string> ldifOutput) where T : ADBaseObjectProperties, new() {
            return await Task.Run<List<T>>(() => {
                return DeserializeObjectsFromLdif<T>(ldifOutput);
            });
        }
        public static List<T> DeserializeObjectsFromLdif<T>(IEnumerable<string> ldifOutput) where T : ADBaseObjectProperties, new() {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} object type: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                CustomExtensions.CurrentMethodName(), typeof(T).Name));

            var results = new List<T>();
            var objectsToDeserialize = new List<List<string>>();
            var intervalObjectToDeserialize = new List<string>();

            // split ldif export by dn: header
            foreach (var line in ldifOutput) {
                // we are starting a new object
                if (line.StartsWith("dn: ", StringComparison.OrdinalIgnoreCase)) {
                    // if we had a previous object in the list, save it
                    if (intervalObjectToDeserialize.Count > 0) {
                        objectsToDeserialize.Add(new List<string>(intervalObjectToDeserialize));
                        intervalObjectToDeserialize.Clear();
                    }
                }
                intervalObjectToDeserialize.Add(line);
            }

            // add the last object
            if (intervalObjectToDeserialize.Count > 0) {
                objectsToDeserialize.Add(intervalObjectToDeserialize);
            }

            foreach (var singleObjectToDeserialize in objectsToDeserialize) {
                var createdObject = Activator.CreateInstance(typeof(T), new object[] { singleObjectToDeserialize }) as T;

                if (createdObject != null) {
                    results.Add(createdObject);
                }
            }

            return results;
        }

        /// <summary>
        /// Exports Active Directory objects using ldifde
        /// </summary>
        public static async Task ExportObjectsToLdifAsync(string dn, string path) {
            await Task.Run(() => {
                ExportObjectsToLdif(dn, path);
            });
        }
        public static void ExportObjectsToLdif(string dn, string path) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            var lines = new List<string>();
            int commandTimeoutSeconds = 30;

            string domain = GetDomainNetBiosNameFromDNOrDNSName(dn);
            var availableDC = ConnectionManager.SelectDC(domain);
            string dcName = availableDC.DNSName;

            // exclude attributes that are protected and cannot be imported again
            string excludedAttributes = "badPasswordTime,badPwdCount,lastLogoff,lastLogon,logonCount,memberOf,objectGUID,objectSid,primaryGroupID,pwdLastSet,sAMAccountType";

            try {
                var processStartInfo = new ProcessStartInfo();
                processStartInfo.RedirectStandardError = true;
                processStartInfo.RedirectStandardOutput = true;
                processStartInfo.UseShellExecute = false;
                processStartInfo.FileName = "ldifde.exe";
                processStartInfo.Arguments = string.Format(" -d \"{0}\" -f \"{1}\" -v -s {2} -o {3}", dn, path, dcName, excludedAttributes);

                System.Diagnostics.Process process = new System.Diagnostics.Process();
                process.StartInfo = processStartInfo;

                process.Start();

                // read standard output before signaling to wait, to prevent a deadlock
                while (!process.StandardOutput.EndOfStream) {
                    lines.Add(process.StandardOutput.ReadLine());
                }

                process.WaitForExit(commandTimeoutSeconds * 1000);
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));

                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Command output: {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), lines.ToFormattedString()));
            }
        }
        #endregion

        #region Query methods
        public static T FindByMailboxGuid<T>(Guid objectGuid) where T : UserAccount, new() {
            var filter = string.Format("mailboxGuid={0}", objectGuid.ToOctectString());
            var results = GetADObjectProperties<T>(ForestRootDN, null, filter, PartitionScope.GlobalCatalog);
            return results.FirstOrDefault();
        }
        public static T FindByObjectGuid<T>(Guid objectGuid) where T : ADBaseObjectProperties, new() {
            var filter = string.Format("objectGuid={0}", objectGuid.ToOctectString());
            var results = GetADObjectProperties<T>(ForestRootDN, null, filter, PartitionScope.GlobalCatalog);
            return results.FirstOrDefault();
        }

        /// <summary>
        /// Wildcards are not allowed in distinguished name searches.  This query uses the supplied prefix
        /// to construct complete distinguished names in each domain, and returns a list of objects that match.
        /// </summary>
        public static List<ADBaseObjectProperties> FindDN(string dnPrefix) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                            dnPrefix ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(dnPrefix)) {
                throw new ArgumentNullException("dnPrefix");
            }
            #endregion

            var adObjects = new List<ADBaseObjectProperties>();
            //Key: BaseDN  Value: DN to search
            var dnsToSearch = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            foreach (var domainInfo in ADDomainInfo) {
                dnsToSearch.Add(domainInfo.DistinguishedName, dnPrefix + domainInfo.DistinguishedName);
            }

            foreach (var kvp in dnsToSearch) {
                string baseDN = kvp.Key;
                string dnToSearch = kvp.Value;

                string adjustedBindPath = string.Empty;
                string filter = string.Format("distinguishedName={0}", dnToSearch);

                adjustedBindPath = GetAdjustedBindPath(baseDN, PartitionScope.LocalPartition);
                List<ADBaseObjectProperties> domainObjectList = GetADObjectProperties<ADBaseObjectProperties>(
                    baseDN, null, filter, PartitionScope.GlobalCatalog);

                if ((domainObjectList != null) && (domainObjectList.Count > 0)) {
                    adObjects.AddRange(domainObjectList);
                }
            }

            return adObjects;
        }

        private static List<T> GetADObjectPropertiesThreadEntry<T>(DomainConfiguration domainInfo, string[] propertiesToGet, string filter) where T : ADBaseObjectProperties, new() {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Querying AD Domain: {3} for: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                CustomExtensions.CurrentMethodName(), domainInfo == null ? "NULL" : domainInfo.NetbiosName, typeof(T).Name));

            var domainObjects = new List<T>();
            var stopwatch = Stopwatch.StartNew();

            try {
                domainObjects = ADDataAccess.GetADObjectProperties<T>(
                    domainInfo.DistinguishedName, propertiesToGet, filter, PartitionScope.LocalPartition);

                if (domainObjects != null) {
                    Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Domain: {3} Objects: {4}",
                        DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                        CustomExtensions.CurrentMethodName(), domainInfo.NetbiosName, domainObjects.Count));
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                    CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }
            finally {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Finished querying AD Domain: {3}.  Time required: {4}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                    domainInfo.NetbiosName, stopwatch.Elapsed));
            }

            return domainObjects;
        }

        /// <summary>
        /// Gets the properties of an AD object as a custom object
        /// </summary>
        /// <param name="baseDN">Can be either the object DN or a base DN</param>
        /// <param name="propertiesToGet">array of LDAP attribute names to get</param>
        /// <param name="filter">LDAP search filter string</param>
        /// <param name="useGC">Use Global Catalog</param>
        /// <param name="destroyIntermediateResults">Destroys the searchResult or DirectoryEntry.  Default = true to minimize resouce consumption.</param>
        /// <param name="cacheResults">Caches results on the local computer.  Default = true.</param>
        /// <param name="trimStringProperties">
        /// Trims the string properties of an object.  Do not use if the actual value of the object is required or 
        /// if comparisons with the actual value will be made.  Note that an objects string properties can be trimmed by calling 
        /// adObject.TrimStringProperties() at any time.
        /// </param>
        /// <remarks>When searching by objectGuid=, encode the guid using ADDataAccess.ToOctectString(guid)</remarks>
        public static async Task<List<T>> GetADObjectPropertiesAsync<T>(MethodParameters adSearchCriteria)
             where T : ADBaseObjectProperties, new() {
            return await Task.Run(() => {
                return GetADObjectProperties<T>(adSearchCriteria);
            });
        }
        public static async Task<List<T>> GetADObjectPropertiesAsync<T>(string baseDN, string[] propertiesToGet = null, string filter = null, PartitionScope partitionScope = PartitionScope.LocalPartition, bool destroyIntermediateResults = true, bool cacheResults = true, bool trimStringProperties = false)
            where T : ADBaseObjectProperties, new() {
            return await Task.Run(() => {
                return GetADObjectProperties<T>(baseDN, propertiesToGet, filter, partitionScope, destroyIntermediateResults, cacheResults, trimStringProperties);
            });
        }

        public static List<T> GetADObjectProperties<T>(string baseDN, string[] propertiesToGet = null, string filter = null, PartitionScope partitionScope = PartitionScope.LocalPartition, bool destroyIntermediateResults = true, bool cacheResults = true, bool trimStringProperties = false)
            where T : ADBaseObjectProperties, new() {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} baseDN: {3} Filter: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                baseDN ?? "NULL", filter ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(baseDN)) {
                throw new ArgumentNullException("baseDN");
            }
            if (!string.IsNullOrWhiteSpace(filter)) {
                if ((filter.StartsWith("(") && !filter.EndsWith(")")) || (filter.EndsWith("(") && !filter.StartsWith(")"))) {
                    throw new ArgumentException(string.Format("filter parenthesis are unbalanced: {0}", filter));
                }
            }
            #endregion

            filter = GetObjectClassObjectCategoryForSearchFilter<T>(filter);

            // do not handle exception for each result.  If exception occurs, re-run the entire operation

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                BaseDN = baseDN,
                PropertiesToGet = propertiesToGet,
                Filter = filter,
                PartitionScope = partitionScope,
                CacheResults = cacheResults,
                DestroyIntermediateResults = destroyIntermediateResults,
                TrimStringProperties = trimStringProperties
            };

            return GetADObjectProperties<T>(methodParameters);
        }
        public static List<T> GetADObjectProperties<T>(MethodParameters methodParameters)
            where T : ADBaseObjectProperties, new() {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} baseDN: {3} Filter: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                (methodParameters != null && methodParameters.BaseDN != null) ? methodParameters.BaseDN : "NULL",
                (methodParameters != null && methodParameters.Filter != null) ? methodParameters.Filter : "NULL"));

            #region Validation
            if (methodParameters == null) {
                throw new ArgumentNullException("methodParameters");
            }
            if (string.IsNullOrWhiteSpace(methodParameters.BaseDN)) {
                throw new ArgumentNullException("baseDN");
            }
            #endregion

            List<T> adObjectList = null;

            // do not handle exception for each result.  If exception occurs, re-run the entire operation

            ConnectionManager.DirectoryServerHandler<MethodParameters, List<T>>(
            ref methodParameters,
                (x) => {
                    adObjectList = new List<T>();
                    List<SearchResult> results = GetSearchResults(methodParameters);

                    if (results == null) return null;
                    foreach (var result in results) {
                        if (result == null) continue;
                        methodParameters.ServerNameOrAdjustedBindPath = result.Path;

                        if (ExcludedDomains.Any(excludedDomain => Regex.IsMatch(result.Path, string.Format("DC={0}", excludedDomain), RegexOptions.IgnoreCase))) continue;

                        var adObject = Activator.CreateInstance(typeof(T), new object[] { result, methodParameters.DestroyIntermediateResults }) as T;

                        if (adObject.IsValid) {
                            if (methodParameters.TrimStringProperties) {
                                adObject.TrimStringProperties();
                            }
                            adObjectList.Add(adObject);
                        }
                    }

                    return adObjectList;
                });

            return adObjectList;
        }

        /// <summary>
        /// Use a threadpool to query all domains for all objects of the specified type and filter.
        /// Useful to get attributes that are not part of the partial attribute set.
        /// </summary>
        /// <remarks>
        /// Warning: GetObjectSecurity and GetObjectReplicationMetadata may require a lot of memory (2x or 3x).  Using those options
        /// for large domains with a lot of objects may be unfeasible.
        /// </remarks>
        public static async Task<List<T>> GetAllADObjectPropertiesAsync<T>(string[] propertiesToGet = null, string filter = null, string forestDnsName = "", bool getObjectSecurity = false) where T : ADBaseObjectProperties, new() {
            return await Task.Run<List<T>>(() => {
                return GetAllADObjectProperties<T>(propertiesToGet, filter, forestDnsName, getObjectSecurity);
            });
        }
        public static List<T> GetAllADObjectProperties<T>(string[] propertiesToGet = null, string filter = null, string forestDnsName = "", bool getObjectSecurity = false) where T : ADBaseObjectProperties, new() {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName()));

            var stopwatch = Stopwatch.StartNew();
            var timeout = TimeSpan.FromHours(1);
            List<T> objects = null;
            var tasks = new List<Task<List<T>>>();

            try {
                objects = new List<T>();
                using (var cts = new CancellationTokenSource()) {
                    foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                        if (!string.IsNullOrWhiteSpace(forestDnsName)) {
                            if (!domainInfo.DNSName.EndsWith(forestDnsName, StringComparison.OrdinalIgnoreCase)) continue;
                        }
                        tasks.Add(Task<List<T>>.Run<List<T>>(() => {
                            return GetADObjectPropertiesThreadEntry<T>(domainInfo, propertiesToGet, filter);
                        }, cts.Token));
                    }
                    if (!Task.WaitAll(tasks.ToArray(), timeout)) {
                        cts.Cancel();
                        throw new ApplicationException("Timeout waiting for threadpool threads to complete.");
                    }

                    foreach (var task in tasks) {
                        if (task.IsFaulted) {
                            foreach (var exception in task.Exception.InnerExceptions) {
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                                    CustomExtensions.CurrentMethodName(), exception.VerboseExceptionString()));
                            }
                        }
                        if (task.IsCompleted && !task.IsFaulted) {
                            objects.AddRange(task.Result);
                        }
                    }

                    if (getObjectSecurity) {
                        var objectSecurityTasks = new List<Task>();
                        foreach (var item in objects) {
                            objectSecurityTasks.Add(Task.Run(() => {
                                item.GetObjectSecurity();
                            }, cts.Token));
                        }

                        if (!Task.WaitAll(objectSecurityTasks.ToArray(), timeout)) {
                            cts.Cancel();
                            throw new ApplicationException("Timeout waiting for threadpool threads to complete getting object security.");
                        }
                    }

                }

                if (objects.Count > 0) {
                    objects = objects
                            .OrderBy(x => x.Domain)
                            .ThenBy(x => x.TopLevelOU)
                            .ToList();
                }
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                    CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }
            finally {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Finished. Time required: {3}; objects returned: {4}",
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                    CustomExtensions.CurrentMethodName(), stopwatch.Elapsed, objects.Count));
            }

            return objects;
        }

        /// <summary>
        /// Prefer GetADObjectProperties<T>() or GetAllADObjectProperties<T>.  This method should only be used for returning non-typed raw SearchResult.
        /// </summary>
        /// <param name="dn">The distinguished name of the object or base dn</param>
        /// <returns>The collection of SearchResult</returns>
        internal static List<SearchResult> GetSearchResults(string dn, string[] propertiesToGet = null, string filter = "", PartitionScope partitionScope = PartitionScope.LocalPartition, bool cacheResults = false) {
            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                BaseDN = dn,
                PropertiesToGet = propertiesToGet,
                Filter = filter,
                PartitionScope = partitionScope,
                CacheResults = cacheResults
            };

            return GetSearchResults(methodParameters);
        }
        internal static List<SearchResult> GetSearchResults(MethodParameters methodParameters) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                methodParameters.BaseDN != null ? methodParameters.BaseDN : "NULL", methodParameters.Filter != null ? methodParameters.Filter : "NULL"));

            #region Validation
            if (methodParameters == null) {
                throw new ArgumentNullException("methodParameters");
            }
            if (string.IsNullOrWhiteSpace(methodParameters.BaseDN)) {
                throw new ArgumentNullException("methodParameters.BaseDN");
            }
            if (methodParameters.PropertiesToGet != null) {
                if (methodParameters.PropertiesToGet.Any(x => string.IsNullOrWhiteSpace(x))) {
                    throw new ArgumentException("methodParameters.PropertiesToGet: one or more elements is null or whitespace.");
                }

                if (ADSchemaAttributes.Count > 0) {
                    foreach (var attribute in methodParameters.PropertiesToGet) {
                        if (!ADSchemaAttributes.ContainsKey(attribute)) {
                            throw new ArgumentException(string.Format("Attribute: {0} not found in schema.", attribute));
                            //Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4} Warning: Query for attribute that is not in schema: {5}",
                            //    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                            //    methodParameters.BaseDN, methodParameters.Filter ?? "NULL", attribute));
                        }
                        else if (methodParameters.PartitionScope == PartitionScope.GlobalCatalog) {
                            if (!ADSchemaAttributes[attribute].IsMemberOfPartialAttributeSet.HasValue || !ADSchemaAttributes[attribute].IsMemberOfPartialAttributeSet.Value) {
                                throw new ArgumentException(string.Format("PartitionScope.GlobalCatalog specified with Attribute: {0} not in Global Catalog.", attribute));
                                //Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4} Warning: Query PartitionScope.GlobalCatalog specified for attribute not in Global Catalog. {5}",
                                //    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                                //    methodParameters.BaseDN, methodParameters.Filter ?? "NULL", attribute));
                            }
                        }
                    }
                }
            }
            #endregion

            var searchResults = new List<SearchResult>();
            ConnectionManager.DirectoryServerHandler<MethodParameters, List<SearchResult>>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(methodParameters.BaseDN, methodParameters.PartitionScope);
                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes))
                    using (var directorySearcher = new DirectorySearcher(directoryEntry)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DirectorySearcher root: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), directorySearcher.SearchRoot.Path));

                        // check for the existence of at least one attribute (objectGuid).  If the object does not have any attributes, we probably do not have permissions.
                        string adjustedFilter = !string.IsNullOrWhiteSpace(methodParameters.Filter)
                            ? ADDataAccess.AdjustADSearchFilter(methodParameters.Filter)
                            : "(objectGuid=*)";
                        directorySearcher.Filter = adjustedFilter;
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Adjusted search filter: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), adjustedFilter));

                        directorySearcher.SearchScope = SearchScope.Subtree;
                        directorySearcher.ReferralChasing = ReferralChasingOption.All;
                        directorySearcher.CacheResults = methodParameters.CacheResults;
                        directorySearcher.Tombstone = ADDataAccess.UseTombstonedObjects;
                        // influence AD into providing more results than the server limit, should page silently in the background
                        directorySearcher.PageSize = MaxPageSize;

                        if ((methodParameters.PropertiesToGet != null) && (methodParameters.PropertiesToGet.Length > 0)) {
                            directorySearcher.PropertiesToLoad.AddRange(AddRequiredQueryAttributes(methodParameters.PropertiesToGet));
                        }

                        using (var searchResultCollection = directorySearcher.FindAll()) {
                            foreach (SearchResult result in searchResultCollection) {
                                searchResults.Add(result);
                            }
                        }

                        return searchResults;
                    } // using (var directorySearcher = new DirectorySearcher(directoryEntry))
                });

            return searchResults;
        }

        /// <summary>
        /// Returns a collection of property names and values for the specified object.
        /// </summary>
        /// <param name="dn">The distinguished name</param>
        /// <param name="propertiesToGet">The properties to get</param>
        /// <param name="filter">The search filter</param>
        /// <param name="partitionScope">Local or Global catalog partition</param>
        /// <param name="cacheResults">True to cache results</param>
        /// <param name="placeHolder">Used to distinguish between methods</param>
        /// <returns>The collection of property names and values</returns>
        public static Dictionary<string, object[]> GetObjectPropertyValuesCollection(string dn, string[] propertiesToGet = null, string filter = "", PartitionScope partitionScope = PartitionScope.LocalPartition, bool cacheResults = false) {
            var attributes = new Dictionary<string, object[]>(StringComparer.OrdinalIgnoreCase);
            var searchResult = ADDataAccess.GetSearchResult(dn, propertiesToGet, filter, partitionScope, cacheResults);
            if (searchResult != null) {
                foreach (string propertyName in searchResult.Properties.PropertyNames) {
                    if (searchResult.Properties[propertyName] == null) continue;
                    if (!attributes.ContainsKey(propertyName)) {
                        object[] element = new object[searchResult.Properties[propertyName].Count];
                        for (int index = 0; index < searchResult.Properties[propertyName].Count; index++) element[index] = searchResult.Properties[propertyName][index];
                        attributes.Add(propertyName, element);
                    }
                }
            }
            return attributes;
        }

        /// <summary>
        /// Prefer GetADObjectProperties<T>() or GetAllADObjectProperties<T>.  This method should only be used for returning non-typed raw SearchResult.
        /// </summary>
        /// <param name="dn">The distinguished name of the object</param>
        /// <returns>The SearchResult</returns>
        internal static SearchResult GetSearchResult(string dn, string[] propertiesToGet = null, string filter = "", PartitionScope partitionScope = PartitionScope.LocalPartition, bool cacheResults = false) {
            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                BaseDN = dn,
                PropertiesToGet = propertiesToGet,
                Filter = filter,
                PartitionScope = partitionScope,
                CacheResults = cacheResults
            };

            return GetSearchResult(methodParameters);
        }
        internal static SearchResult GetSearchResult(MethodParameters methodParameters) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                methodParameters.BaseDN != null ? methodParameters.BaseDN : "NULL", methodParameters.Filter != null ? methodParameters.Filter : "NULL"));

            #region Validation
            if (methodParameters == null) {
                throw new ArgumentNullException("methodParameters");
            }
            if (string.IsNullOrWhiteSpace(methodParameters.BaseDN)) {
                throw new ArgumentNullException("methodParameters.BaseDN");
            }
            if (methodParameters.PropertiesToGet != null) {
                if (methodParameters.PropertiesToGet.Any(x => string.IsNullOrWhiteSpace(x))) {
                    throw new ApplicationException("methodParameters.PropertiesToGet: one or more elements is null or whitespace.");
                }

                if (ADSchemaAttributes.Count > 0) {
                    foreach (var attribute in methodParameters.PropertiesToGet) {
                        if (!ADSchemaAttributes.ContainsKey(attribute)) {
                            throw new ApplicationException(string.Format("Attribute: {0} not found in schema.", attribute));
                            //Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4} Warning: Query for attribute that is not in schema: {5}",
                            //    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                            //    methodParameters.BaseDN, methodParameters.Filter ?? "NULL", attribute));
                        }
                        else if (methodParameters.PartitionScope == PartitionScope.GlobalCatalog) {
                            if (!ADSchemaAttributes[attribute].IsMemberOfPartialAttributeSet.HasValue || !ADSchemaAttributes[attribute].IsMemberOfPartialAttributeSet.Value) {
                                throw new ApplicationException(string.Format("PartitionScope.GlobalCatalog specified with Attribute: {0} not in Global Catalog.", attribute));
                                //Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3} Filter: {4} Warning: Query PartitionScope.GlobalCatalog specified for attribute not in Global Catalog. {5}",
                                //    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                                //    methodParameters.BaseDN, methodParameters.Filter ?? "NULL", attribute));
                            }
                        }
                    }
                }
            }
            #endregion

            SearchResult result =
            ConnectionManager.DirectoryServerHandler<MethodParameters, SearchResult>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(methodParameters.BaseDN, methodParameters.PartitionScope);
                    using (var directoryEntry = new DirectoryEntry(methodParameters.ServerNameOrAdjustedBindPath, null, null, methodParameters.AuthenticationTypes))
                    using (var directorySearcher = new DirectorySearcher(directoryEntry)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DirectorySearcher root: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                            directorySearcher.SearchRoot.Path));

                        // check for the existence of at least one attribute (objectGuid).  If the object does not have any attributes, we probably do not have permissions.
                        string adjustedFilter = !string.IsNullOrWhiteSpace(methodParameters.Filter)
                            ? ADDataAccess.AdjustADSearchFilter(methodParameters.Filter)
                            : "(objectGuid=*)";
                        directorySearcher.Filter = adjustedFilter;

                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Adjusted search filter: {3} PropertiesToGet count; {4}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                            CustomExtensions.CurrentMethodName(), adjustedFilter, methodParameters.PropertiesToGet == null ? "(All)" : methodParameters.PropertiesToGet.Length.ToString()));

                        directorySearcher.SearchScope = SearchScope.Subtree;
                        directorySearcher.ReferralChasing = ReferralChasingOption.All;
                        directorySearcher.CacheResults = methodParameters.CacheResults;
                        directorySearcher.Tombstone = ADDataAccess.UseTombstonedObjects;

                        if ((methodParameters.PropertiesToGet != null) && (methodParameters.PropertiesToGet.Length > 0)) {
                            directorySearcher.PropertiesToLoad.AddRange(AddRequiredQueryAttributes(methodParameters.PropertiesToGet));
                        }

                        return directorySearcher.FindOne();

                    }
                });

            return result;
        }

        /// <summary>
        /// Gets the ReplicationAttributeMetaData for the specified dn
        /// </summary>
        /// <param name="dn">The distinguished name of the object</param>
        /// <returns>Collection of ReplicationAttributeMetaData</returns>
        /// <remarks>Requires elevated permissions</remarks>
        public static IReadOnlyList<ReplicationAttributeMetaData> GetReplicationAttributeMetaData(string dn) {

            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }

            var replicationAttributeMetaData = new List<ReplicationAttributeMetaData>();
            var propertiesToGet = new string[] { "msDS-ReplAttributeMetaData" };
            var filter = string.Format("distinguishedName={0}", dn);
            var objectWithMetaData = ADDataAccess.GetADObjectProperties<ADBaseObjectProperties>(
                dn, propertiesToGet, filter, PartitionScope.LocalPartition).FirstOrDefault();
            if ((objectWithMetaData != null) && (objectWithMetaData.ReplicationAttributeMetadata != null)) {
                return objectWithMetaData.ReplicationAttributeMetadata;
            }

            return replicationAttributeMetaData;
        }

        /// <summary>
        /// Gets the ReplicationValueMetaData for the specified dn
        /// </summary>
        /// <param name="dn">The distinguished name of the object</param>
        /// <returns>Collection of ReplicationValueMetaData</returns>
        /// <remarks>Requires elevated permissions. Replication value is for linked value attributes.</remarks>
        public static IReadOnlyList<ReplicationValueMetaData> GetReplicationValueMetaData(string dn) {

            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }

            var replicationValueMetaData = new List<ReplicationValueMetaData>();
            var propertiesToGet = new string[] { "msDS-ReplValueMetaData" };
            var filter = string.Format("distinguishedName={0}", dn);
            var objectWithMetaData = ADDataAccess.GetADObjectProperties<ADBaseObjectProperties>(
                dn, propertiesToGet, filter, PartitionScope.LocalPartition).FirstOrDefault();
            if ((objectWithMetaData != null) && (objectWithMetaData.ReplicationValueMetadata != null)) {
                return objectWithMetaData.ReplicationValueMetadata;
            }

            return replicationValueMetaData;
        }

        public static bool ObjectExists(string dn) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} DN: {3}",
                DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                dn ?? "NULL"));

            #region Validation
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ApplicationException(string.Format("Invalid DN format: {0}", dn ?? "NULL"));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                ObjectDN = dn
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, bool>(
                ref methodParameters,
                (x) => {
                    methodParameters.ServerNameOrAdjustedBindPath = GetAdjustedBindPath(methodParameters.ObjectDN, PartitionScope.LocalPartition);
                    return DirectoryEntry.Exists(methodParameters.ServerNameOrAdjustedBindPath);
                });
        }
        #endregion
        #endregion
    }
}
